mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-13 03:32:46 +00:00
Compare commits
49 Commits
v1.0-beta.
...
v1.0-beta.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4779a21134 | ||
|
|
6215c3c916 | ||
|
|
16ce7942be | ||
|
|
3a2e5f884b | ||
|
|
2b6a58ff04 | ||
|
|
72e825a12c | ||
|
|
8aeeba0eeb | ||
|
|
2952dd2c63 | ||
|
|
908d64bd4f | ||
|
|
41a185bd9f | ||
|
|
61dd2e1f74 | ||
|
|
3bb140aeb3 | ||
|
|
7172510c9e | ||
|
|
cfe2875a67 | ||
|
|
9860df2379 | ||
|
|
cc459f9fea | ||
|
|
9e6a25ec1d | ||
|
|
6304d8178f | ||
|
|
500a64de18 | ||
|
|
55a2223198 | ||
|
|
3beb716b83 | ||
|
|
ed6e3c56f2 | ||
|
|
579e65b0be | ||
|
|
b3dcdda697 | ||
|
|
44a9ff57e7 | ||
|
|
4361b90425 | ||
|
|
3471ecb21a | ||
|
|
5a9b98fd06 | ||
|
|
34e5dc6a20 | ||
|
|
8f8c60e0b3 | ||
|
|
9b774d640c | ||
|
|
4a0aee1c44 | ||
|
|
97fbd3226d | ||
|
|
0f43b3ee66 | ||
|
|
b469da70e8 | ||
|
|
10e33de897 | ||
|
|
6e8fb81ade | ||
|
|
34a09ad15d | ||
|
|
201a235357 | ||
|
|
e68b8f7e7c | ||
|
|
a5b3a12914 | ||
|
|
3ed30b206c | ||
|
|
1344199807 | ||
|
|
33a189fe91 | ||
|
|
2f361db9df | ||
|
|
bf6cd6cd46 | ||
|
|
54266d1ba3 | ||
|
|
d3894d2a9f | ||
|
|
00c089e677 |
5
.github/workflows/publish.yml
vendored
5
.github/workflows/publish.yml
vendored
@@ -10,8 +10,9 @@
|
||||
name: Publish Docker images
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
|
||||
@@ -43,6 +43,8 @@ namespace Notesnook.API.Accessors
|
||||
public SyncItemsRepository Tags { get; }
|
||||
public Repository<UserSettings> UsersSettings { get; }
|
||||
public Repository<Monograph> Monographs { get; }
|
||||
public Repository<InboxApiKey> InboxApiKey { get; }
|
||||
public SyncItemsRepository InboxItems { get; }
|
||||
|
||||
public SyncItemsRepositoryAccessor(IDbContext dbContext,
|
||||
|
||||
@@ -70,11 +72,15 @@ 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<UserSettings> usersSettings, Repository<Monograph> monographs,
|
||||
Repository<InboxApiKey> inboxApiKey)
|
||||
{
|
||||
UsersSettings = usersSettings;
|
||||
Monographs = monographs;
|
||||
InboxApiKey = inboxApiKey;
|
||||
Notebooks = new SyncItemsRepository(dbContext, notebooks);
|
||||
Notes = new SyncItemsRepository(dbContext, notes);
|
||||
Contents = new SyncItemsRepository(dbContext, content);
|
||||
@@ -87,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Notesnook.API.Authorization
|
||||
{
|
||||
public class ProUserRequirement : AuthorizationHandler<ProUserRequirement>, IAuthorizationRequirement
|
||||
{
|
||||
private readonly Dictionary<string, string> pathErrorPhraseMap = new()
|
||||
{
|
||||
["/s3"] = "upload attachments",
|
||||
["/s3/multipart"] = "upload attachments",
|
||||
};
|
||||
private readonly string[] allowedClaims = ["trial", "premium", "premium_canceled"];
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ProUserRequirement requirement)
|
||||
{
|
||||
PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null;
|
||||
var isProOrTrial = context.User.Claims.Any((c) => c.Type == "notesnook:status" && allowedClaims.Contains(c.Value));
|
||||
if (isProOrTrial) context.Succeed(requirement);
|
||||
else
|
||||
{
|
||||
var phrase = "continue";
|
||||
foreach (var item in pathErrorPhraseMap)
|
||||
{
|
||||
if (path != null && path.StartsWithSegments(item.Key))
|
||||
phrase = item.Value;
|
||||
}
|
||||
var error = $"Please upgrade to Pro to {phrase}.";
|
||||
context.Fail(new AuthorizationFailureReason(this, error));
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task HandleAsync(AuthorizationHandlerContext context)
|
||||
{
|
||||
return this.HandleRequirementAsync(context, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +1,102 @@
|
||||
/*
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization.Policy;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Notesnook.API.Authorization
|
||||
{
|
||||
public class SyncRequirement : AuthorizationHandler<SyncRequirement>, IAuthorizationRequirement
|
||||
{
|
||||
private readonly Dictionary<string, string> pathErrorPhraseMap = new()
|
||||
{
|
||||
["/sync/attachments"] = "use attachments",
|
||||
["/sync"] = "sync your notes",
|
||||
["/hubs/sync"] = "sync your notes",
|
||||
["/hubs/sync/v2"] = "sync your notes",
|
||||
["/monographs"] = "publish monographs"
|
||||
};
|
||||
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncRequirement requirement)
|
||||
{
|
||||
PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null;
|
||||
var result = this.IsAuthorized(context.User, path);
|
||||
if (result.Succeeded) context.Succeed(requirement);
|
||||
else if (result.AuthorizationFailure.FailureReasons.Any())
|
||||
context.Fail(result.AuthorizationFailure.FailureReasons.First());
|
||||
else context.Fail();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public PolicyAuthorizationResult IsAuthorized(ClaimsPrincipal User, PathString requestPath)
|
||||
{
|
||||
var id = User.FindFirstValue("sub");
|
||||
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
var reason = new[]
|
||||
{
|
||||
new AuthorizationFailureReason(this, "Invalid token.")
|
||||
};
|
||||
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
|
||||
}
|
||||
|
||||
var hasSyncScope = User.HasClaim("scope", "notesnook.sync");
|
||||
var isInAudience = User.HasClaim("aud", "notesnook");
|
||||
var hasRole = User.HasClaim("role", "notesnook");
|
||||
|
||||
var isEmailVerified = User.HasClaim("verified", "true");
|
||||
|
||||
if (!isEmailVerified)
|
||||
{
|
||||
var phrase = "continue";
|
||||
|
||||
foreach (var item in pathErrorPhraseMap)
|
||||
{
|
||||
if (requestPath != null && requestPath.StartsWithSegments(item.Key))
|
||||
phrase = item.Value;
|
||||
}
|
||||
|
||||
var error = $"Please confirm your email to {phrase}.";
|
||||
var reason = new[]
|
||||
{
|
||||
new AuthorizationFailureReason(this, error)
|
||||
};
|
||||
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
|
||||
// context.Fail(new AuthorizationFailureReason(this, error));
|
||||
}
|
||||
|
||||
if (hasSyncScope && isInAudience && hasRole && isEmailVerified)
|
||||
return PolicyAuthorizationResult.Success(); //(requirement);
|
||||
return PolicyAuthorizationResult.Forbid();
|
||||
}
|
||||
|
||||
public override Task HandleAsync(AuthorizationHandlerContext context)
|
||||
{
|
||||
return this.HandleRequirementAsync(context, this);
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization.Policy;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Notesnook.API.Authorization
|
||||
{
|
||||
public class SyncRequirement : AuthorizationHandler<SyncRequirement>, IAuthorizationRequirement
|
||||
{
|
||||
private readonly Dictionary<string, string> pathErrorPhraseMap = new()
|
||||
{
|
||||
["/sync/attachments"] = "use attachments",
|
||||
["/sync"] = "sync your notes",
|
||||
["/hubs/sync"] = "sync your notes",
|
||||
["/hubs/sync/v2"] = "sync your notes",
|
||||
["/monographs"] = "publish monographs"
|
||||
};
|
||||
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncRequirement requirement)
|
||||
{
|
||||
PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null;
|
||||
var result = this.IsAuthorized(context.User, path);
|
||||
if (result.Succeeded) context.Succeed(requirement);
|
||||
else if (result.AuthorizationFailure.FailureReasons.Any())
|
||||
context.Fail(result.AuthorizationFailure.FailureReasons.First());
|
||||
else context.Fail();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public PolicyAuthorizationResult IsAuthorized(ClaimsPrincipal? User, PathString requestPath)
|
||||
{
|
||||
var id = User?.FindFirstValue("sub");
|
||||
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
var reason = new[]
|
||||
{
|
||||
new AuthorizationFailureReason(this, "Invalid token.")
|
||||
};
|
||||
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
|
||||
}
|
||||
|
||||
var hasSyncScope = User.HasClaim("scope", "notesnook.sync");
|
||||
var isInAudience = User.HasClaim("aud", "notesnook");
|
||||
var hasRole = User.HasClaim("role", "notesnook");
|
||||
|
||||
var isEmailVerified = User.HasClaim("verified", "true");
|
||||
|
||||
if (!isEmailVerified)
|
||||
{
|
||||
var phrase = "continue";
|
||||
|
||||
foreach (var item in pathErrorPhraseMap)
|
||||
{
|
||||
if (requestPath != null && requestPath.StartsWithSegments(item.Key))
|
||||
phrase = item.Value;
|
||||
}
|
||||
|
||||
var error = $"Please confirm your email to {phrase}.";
|
||||
var reason = new[]
|
||||
{
|
||||
new AuthorizationFailureReason(this, error)
|
||||
};
|
||||
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
|
||||
// context.Fail(new AuthorizationFailureReason(this, error));
|
||||
}
|
||||
|
||||
if (hasSyncScope && isInAudience && hasRole && isEmailVerified)
|
||||
return PolicyAuthorizationResult.Success(); //(requirement);
|
||||
return PolicyAuthorizationResult.Forbid();
|
||||
}
|
||||
|
||||
public override Task HandleAsync(AuthorizationHandlerContext context)
|
||||
{
|
||||
return this.HandleRequirementAsync(context, this);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -14,5 +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";
|
||||
}
|
||||
}
|
||||
201
Notesnook.API/Controllers/InboxController.cs
Normal file
201
Notesnook.API/Controllers/InboxController.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
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.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]
|
||||
[Route("inbox")]
|
||||
public class InboxController : ControllerBase
|
||||
{
|
||||
private readonly Repository<InboxApiKey> InboxApiKey;
|
||||
private readonly Repository<UserSettings> UserSetting;
|
||||
private SyncItemsRepository InboxItems;
|
||||
|
||||
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");
|
||||
try
|
||||
{
|
||||
var apiKeys = await InboxApiKey.FindAsync(t => t.UserId == userId);
|
||||
return Ok(apiKeys);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Slogger<InboxController>.Error(nameof(GetApiKeysAsync), "Couldn't get inbox api keys.", userId, ex.ToString());
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("api-keys")]
|
||||
[Authorize(Policy = "Notesnook")]
|
||||
public async Task<IActionResult> 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<InboxController>.Error(nameof(CreateApiKeyAsync), "Couldn't create inbox api key.", userId, ex.ToString());
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("api-keys/{apiKey}")]
|
||||
[Authorize(Policy = "Notesnook")]
|
||||
public async Task<IActionResult> 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<InboxController>.Error(nameof(DeleteApiKeyAsync), "Couldn't delete inbox api key.", userId, ex.ToString());
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,17 @@ using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using Notesnook.API.Authorization;
|
||||
using Notesnook.API.Models;
|
||||
using Notesnook.API.Services;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Data.Interfaces;
|
||||
using Streetwriters.Data.Repositories;
|
||||
@@ -39,21 +43,15 @@ namespace Notesnook.API.Controllers
|
||||
[ApiController]
|
||||
[Route("monographs")]
|
||||
[Authorize("Sync")]
|
||||
public class MonographsController : ControllerBase
|
||||
public class MonographsController(Repository<Monograph> monographs, IURLAnalyzer analyzer) : ControllerBase
|
||||
{
|
||||
const string SVG_PIXEL = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><circle r='9'/></svg>";
|
||||
private Repository<Monograph> Monographs { get; set; }
|
||||
private readonly IUnitOfWork unit;
|
||||
private const int MAX_DOC_SIZE = 15 * 1024 * 1024;
|
||||
public MonographsController(Repository<Monograph> monographs, IUnitOfWork unitOfWork)
|
||||
{
|
||||
Monographs = monographs;
|
||||
unit = unitOfWork;
|
||||
}
|
||||
|
||||
private static FilterDefinition<Monograph> CreateMonographFilter(string userId, Monograph monograph)
|
||||
{
|
||||
var userIdFilter = Builders<Monograph>.Filter.Eq("UserId", userId);
|
||||
monograph.ItemId ??= monograph.Id;
|
||||
return ObjectId.TryParse(monograph.ItemId, out ObjectId id)
|
||||
? Builders<Monograph>.Filter
|
||||
.And(userIdFilter,
|
||||
@@ -78,7 +76,7 @@ namespace Notesnook.API.Controllers
|
||||
|
||||
private async Task<Monograph> FindMonographAsync(string userId, Monograph monograph)
|
||||
{
|
||||
var result = await Monographs.Collection.FindAsync(CreateMonographFilter(userId, monograph), new FindOptions<Monograph>
|
||||
var result = await monographs.Collection.FindAsync(CreateMonographFilter(userId, monograph), new FindOptions<Monograph>
|
||||
{
|
||||
Limit = 1
|
||||
});
|
||||
@@ -87,7 +85,7 @@ namespace Notesnook.API.Controllers
|
||||
|
||||
private async Task<Monograph> FindMonographAsync(string itemId)
|
||||
{
|
||||
var result = await Monographs.Collection.FindAsync(CreateMonographFilter(itemId), new FindOptions<Monograph>
|
||||
var result = await monographs.Collection.FindAsync(CreateMonographFilter(itemId), new FindOptions<Monograph>
|
||||
{
|
||||
Limit = 1
|
||||
});
|
||||
@@ -95,21 +93,19 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> PublishAsync([FromQuery] string deviceId, [FromBody] Monograph monograph)
|
||||
public async Task<IActionResult> PublishAsync([FromQuery] string? deviceId, [FromBody] Monograph monograph)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
var jti = this.User.FindFirstValue("jti");
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var existingMonograph = await FindMonographAsync(userId, monograph);
|
||||
if (existingMonograph != null && !existingMonograph.Deleted)
|
||||
{
|
||||
return base.Conflict("This monograph is already published.");
|
||||
}
|
||||
if (existingMonograph != null && !existingMonograph.Deleted) return await UpdateAsync(deviceId, monograph);
|
||||
|
||||
if (monograph.EncryptedContent == null)
|
||||
monograph.CompressedContent = monograph.Content.CompressBrotli();
|
||||
monograph.CompressedContent = (await CleanupContentAsync(monograph.Content)).CompressBrotli();
|
||||
monograph.UserId = userId;
|
||||
monograph.DatePublished = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
@@ -118,16 +114,16 @@ namespace Notesnook.API.Controllers
|
||||
|
||||
if (existingMonograph != null)
|
||||
{
|
||||
monograph.Id = existingMonograph?.Id;
|
||||
monograph.Id = existingMonograph.Id;
|
||||
}
|
||||
monograph.Deleted = false;
|
||||
await Monographs.Collection.ReplaceOneAsync(
|
||||
await monographs.Collection.ReplaceOneAsync(
|
||||
CreateMonographFilter(userId, monograph),
|
||||
monograph,
|
||||
new ReplaceOptions { IsUpsert = true }
|
||||
);
|
||||
|
||||
await MarkMonographForSyncAsync(monograph.ItemId ?? monograph.Id, deviceId);
|
||||
await MarkMonographForSyncAsync(userId, monograph.ItemId ?? monograph.Id, deviceId, jti);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
@@ -143,11 +139,12 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
|
||||
[HttpPatch]
|
||||
public async Task<IActionResult> UpdateAsync([FromQuery] string deviceId, [FromBody] Monograph monograph)
|
||||
public async Task<IActionResult> UpdateAsync([FromQuery] string? deviceId, [FromBody] Monograph monograph)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
var jti = this.User.FindFirstValue("jti");
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var existingMonograph = await FindMonographAsync(userId, monograph);
|
||||
@@ -160,12 +157,12 @@ namespace Notesnook.API.Controllers
|
||||
return base.BadRequest("Monograph is too big. Max allowed size is 15mb.");
|
||||
|
||||
if (monograph.EncryptedContent == null)
|
||||
monograph.CompressedContent = monograph.Content.CompressBrotli();
|
||||
monograph.CompressedContent = (await CleanupContentAsync(monograph.Content)).CompressBrotli();
|
||||
else
|
||||
monograph.Content = null;
|
||||
|
||||
monograph.DatePublished = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var result = await Monographs.Collection.UpdateOneAsync(
|
||||
var result = await monographs.Collection.UpdateOneAsync(
|
||||
CreateMonographFilter(userId, monograph),
|
||||
Builders<Monograph>.Update
|
||||
.Set(m => m.DatePublished, monograph.DatePublished)
|
||||
@@ -177,7 +174,7 @@ namespace Notesnook.API.Controllers
|
||||
);
|
||||
if (!result.IsAcknowledged) return BadRequest();
|
||||
|
||||
await MarkMonographForSyncAsync(monograph.ItemId ?? monograph.Id, deviceId);
|
||||
await MarkMonographForSyncAsync(userId, monograph.ItemId ?? monograph.Id, deviceId, jti);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
@@ -198,16 +195,16 @@ namespace Notesnook.API.Controllers
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var monographs = (await Monographs.Collection.FindAsync(
|
||||
var userMonographs = (await monographs.Collection.FindAsync(
|
||||
Builders<Monograph>.Filter.And(
|
||||
Builders<Monograph>.Filter.Eq("UserId", userId),
|
||||
Builders<Monograph>.Filter.Eq("Deleted", false)
|
||||
Builders<Monograph>.Filter.Ne("Deleted", true)
|
||||
)
|
||||
, new FindOptions<Monograph, ObjectWithId>
|
||||
{
|
||||
Projection = Builders<Monograph>.Projection.Include("_id").Include("ItemId"),
|
||||
})).ToEnumerable();
|
||||
return Ok(monographs.Select((m) => m.ItemId ?? m.Id));
|
||||
return Ok(userMonographs.Select((m) => m.ItemId ?? m.Id));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@@ -225,8 +222,8 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
|
||||
if (monograph.EncryptedContent == null)
|
||||
monograph.Content = monograph.CompressedContent.DecompressBrotli();
|
||||
if (monograph.ItemId == null) monograph.ItemId = monograph.Id;
|
||||
monograph.Content = monograph.CompressedContent?.DecompressBrotli();
|
||||
monograph.ItemId ??= monograph.Id;
|
||||
return Ok(monograph);
|
||||
}
|
||||
|
||||
@@ -239,38 +236,36 @@ namespace Notesnook.API.Controllers
|
||||
|
||||
if (monograph.SelfDestruct)
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
await Monographs.Collection.ReplaceOneAsync(
|
||||
CreateMonographFilter(userId, monograph),
|
||||
await monographs.Collection.ReplaceOneAsync(
|
||||
CreateMonographFilter(monograph.UserId, monograph),
|
||||
new Monograph
|
||||
{
|
||||
ItemId = id,
|
||||
Id = monograph.Id,
|
||||
Deleted = true
|
||||
Deleted = true,
|
||||
UserId = monograph.UserId
|
||||
}
|
||||
);
|
||||
|
||||
await MarkMonographForSyncAsync(id);
|
||||
await MarkMonographForSyncAsync(monograph.UserId, id);
|
||||
}
|
||||
|
||||
return Content(SVG_PIXEL, "image/svg+xml");
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteAsync([FromQuery] string deviceId, [FromRoute] string id)
|
||||
public async Task<IActionResult> DeleteAsync([FromQuery] string? deviceId, [FromRoute] string id)
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
if (userId is null) return Unauthorized();
|
||||
|
||||
var monograph = await FindMonographAsync(id);
|
||||
if (monograph == null || monograph.Deleted)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
error = "invalid_id",
|
||||
error_description = $"No such monograph found."
|
||||
});
|
||||
}
|
||||
return Ok();
|
||||
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
await Monographs.Collection.ReplaceOneAsync(
|
||||
var jti = this.User.FindFirstValue("jti");
|
||||
|
||||
await monographs.Collection.ReplaceOneAsync(
|
||||
CreateMonographFilter(userId, monograph),
|
||||
new Monograph
|
||||
{
|
||||
@@ -281,33 +276,27 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
);
|
||||
|
||||
await MarkMonographForSyncAsync(id, deviceId);
|
||||
await MarkMonographForSyncAsync(userId, id, deviceId, jti);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private async Task MarkMonographForSyncAsync(string monographId, string deviceId)
|
||||
private static async Task MarkMonographForSyncAsync(string userId, string monographId, string? deviceId, string? jti)
|
||||
{
|
||||
if (deviceId == null) return;
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
|
||||
new SyncDeviceService(new SyncDevice(userId, deviceId)).AddIdsToOtherDevices([$"{monographId}:monograph"]);
|
||||
await SendTriggerSyncEventAsync();
|
||||
await SendTriggerSyncEventAsync(userId, jti);
|
||||
}
|
||||
|
||||
private async Task MarkMonographForSyncAsync(string monographId)
|
||||
private static async Task MarkMonographForSyncAsync(string userId, string monographId)
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
|
||||
new SyncDeviceService(new SyncDevice(userId, string.Empty)).AddIdsToAllDevices([$"{monographId}:monograph"]);
|
||||
await SendTriggerSyncEventAsync(sendToAllDevices: true);
|
||||
await SendTriggerSyncEventAsync(userId, sendToAllDevices: true);
|
||||
}
|
||||
|
||||
private async Task SendTriggerSyncEventAsync(bool sendToAllDevices = false)
|
||||
private static async Task SendTriggerSyncEventAsync(string userId, string? jti = null, bool sendToAllDevices = false)
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
var jti = this.User.FindFirstValue("jti");
|
||||
|
||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
||||
{
|
||||
OriginTokenId = sendToAllDevices ? null : jti,
|
||||
@@ -319,5 +308,50 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<string> CleanupContentAsync(string content)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Deserialize<MonographContent>(content);
|
||||
var html = json.Data;
|
||||
if (!Constants.IS_SELF_HOSTED && !User.IsUserSubscribed())
|
||||
{
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
var context = BrowsingContext.New(config);
|
||||
var document = await context.OpenAsync(r => r.Content(html));
|
||||
foreach (var element in document.QuerySelectorAll("a,iframe,img,object,svg,button,link"))
|
||||
{
|
||||
element.Remove();
|
||||
}
|
||||
html = document.ToHtml();
|
||||
}
|
||||
|
||||
if (User.IsUserSubscribed())
|
||||
{
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
var context = BrowsingContext.New(config);
|
||||
var document = await context.OpenAsync(r => r.Content(html));
|
||||
foreach (var element in document.QuerySelectorAll("a"))
|
||||
{
|
||||
var href = element.GetAttribute("href");
|
||||
if (string.IsNullOrEmpty(href)) continue;
|
||||
if (!await analyzer.IsURLSafeAsync(href)) element.RemoveAttribute("href");
|
||||
}
|
||||
html = document.ToHtml();
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize<MonographContent>(new MonographContent
|
||||
{
|
||||
Type = json.Type,
|
||||
Data = html
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Slogger<MonographsController>.Error("CleanupContentAsync", ex.ToString());
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ using System.Threading.Tasks;
|
||||
using System.Security.Claims;
|
||||
using Notesnook.API.Interfaces;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Streetwriters.Common.Extensions;
|
||||
using Streetwriters.Common.Models;
|
||||
using Notesnook.API.Helpers;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
|
||||
namespace Notesnook.API.Controllers
|
||||
@@ -31,28 +37,65 @@ namespace Notesnook.API.Controllers
|
||||
[ApiController]
|
||||
[Route("s3")]
|
||||
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
|
||||
[Authorize("Sync")]
|
||||
public class S3Controller : ControllerBase
|
||||
{
|
||||
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||
private IS3Service S3Service { get; set; }
|
||||
public S3Controller(IS3Service s3Service)
|
||||
public S3Controller(IS3Service s3Service, ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor)
|
||||
{
|
||||
S3Service = s3Service;
|
||||
Repositories = syncItemsRepositoryAccessor;
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Authorize("Pro")]
|
||||
public IActionResult Upload([FromQuery] string name)
|
||||
public async Task<IActionResult> Upload([FromQuery] string name)
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
|
||||
if (!HttpContext.Request.Headers.ContentLength.HasValue) return BadRequest(new { error = "No Content-Length header found." });
|
||||
|
||||
long fileSize = HttpContext.Request.Headers.ContentLength.Value;
|
||||
if (fileSize == 0)
|
||||
{
|
||||
var uploadUrl = S3Service.GetUploadObjectUrl(userId, name);
|
||||
if (uploadUrl == null) return BadRequest(new { error = "Could not create signed url." });
|
||||
return Ok(uploadUrl);
|
||||
}
|
||||
|
||||
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
|
||||
var subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId);
|
||||
if (subscription is null) return BadRequest(new { error = "User subscription not found." });
|
||||
|
||||
if (StorageHelper.IsFileSizeExceeded(subscription, fileSize))
|
||||
{
|
||||
return BadRequest(new { error = "Max file size exceeded." });
|
||||
}
|
||||
|
||||
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||
userSettings.StorageLimit ??= new Limit { Value = 0, UpdatedAt = 0 };
|
||||
userSettings.StorageLimit.Value += fileSize;
|
||||
if (StorageHelper.IsStorageLimitReached(subscription, userSettings.StorageLimit))
|
||||
return BadRequest(new { error = "Storage limit exceeded." });
|
||||
|
||||
var url = S3Service.GetUploadObjectUrl(userId, name);
|
||||
if (url == null) return BadRequest("Could not create signed url.");
|
||||
return Ok(url);
|
||||
if (url == null) return BadRequest(new { error = "Could not create signed url." });
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
var content = new StreamContent(HttpContext.Request.BodyReader.AsStream());
|
||||
content.Headers.ContentLength = Request.ContentLength;
|
||||
var response = await httpClient.SendRequestAsync<Response>(url, null, HttpMethod.Put, content);
|
||||
if (!response.Success) return BadRequest(await response.Content.ReadAsStringAsync());
|
||||
|
||||
userSettings.StorageLimit.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("multipart")]
|
||||
[Authorize("Pro")]
|
||||
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.FindFirstValue("sub");
|
||||
try
|
||||
@@ -64,7 +107,6 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
|
||||
[HttpDelete("multipart")]
|
||||
[Authorize("Pro")]
|
||||
public async Task<IActionResult> AbortMultipartUpload([FromQuery] string name, [FromQuery] string uploadId)
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
@@ -77,7 +119,6 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("multipart")]
|
||||
[Authorize("Pro")]
|
||||
public async Task<IActionResult> CompleteMultipartUpload([FromBody] CompleteMultipartUploadRequestWrapper uploadRequestWrapper)
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
@@ -90,17 +131,19 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize("Sync")]
|
||||
public IActionResult Download([FromQuery] string name)
|
||||
public async Task<IActionResult> Download([FromQuery] string name)
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
var url = S3Service.GetDownloadObjectUrl(userId, name);
|
||||
if (url == null) return BadRequest("Could not create signed url.");
|
||||
return Ok(url);
|
||||
try
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
var url = await S3Service.GetDownloadObjectUrl(userId, name);
|
||||
if (url == null) return BadRequest("Could not create signed url.");
|
||||
return Ok(url);
|
||||
}
|
||||
catch (Exception ex) { return BadRequest(ex.Message); }
|
||||
}
|
||||
|
||||
[HttpHead]
|
||||
[Authorize("Sync")]
|
||||
public async Task<IActionResult> Info([FromQuery] string name)
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
@@ -110,7 +153,6 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Authorize("Sync")]
|
||||
public async Task<IActionResult> DeleteAsync([FromQuery] string name)
|
||||
{
|
||||
try
|
||||
@@ -125,4 +167,4 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Notesnook.API.Controllers
|
||||
try
|
||||
{
|
||||
UserResponse response = await UserService.GetUserAsync(userId);
|
||||
if (!response.Success) return BadRequest(response);
|
||||
if (!response.Success) return BadRequest();
|
||||
return Ok(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -69,16 +69,11 @@ namespace Notesnook.API.Controllers
|
||||
}
|
||||
|
||||
[HttpPatch]
|
||||
public async Task<IActionResult> UpdateUser([FromBody] UserResponse user)
|
||||
public async Task<IActionResult> UpdateUser([FromBody] UserKeys keys)
|
||||
{
|
||||
var userId = User.FindFirstValue("sub");
|
||||
try
|
||||
{
|
||||
var keys = new UserKeys
|
||||
{
|
||||
AttachmentsKey = user.AttachmentsKey,
|
||||
MonographPasswordsKey = user.MonographPasswordsKey
|
||||
};
|
||||
await UserService.SetUserKeysAsync(userId, keys);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@ 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.Linq;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -42,12 +45,9 @@ namespace Notesnook.API.Extensions
|
||||
AuthorizationPolicy authorizationPolicy,
|
||||
PolicyAuthorizationResult policyAuthorizationResult)
|
||||
{
|
||||
var isWebsocket = httpContext.Request.Headers.Upgrade == "websocket";
|
||||
|
||||
if (!isWebsocket && policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
|
||||
if (policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
|
||||
{
|
||||
var error = string.Join("\n", policyAuthorizationResult.AuthorizationFailure.FailureReasons.Select((r) => r.Message));
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
@@ -55,17 +55,8 @@ namespace Notesnook.API.Extensions
|
||||
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(new { error }));
|
||||
return;
|
||||
}
|
||||
|
||||
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
|
||||
}
|
||||
else if (isWebsocket)
|
||||
{
|
||||
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, PolicyAuthorizationResult.Success());
|
||||
}
|
||||
else
|
||||
{
|
||||
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
|
||||
}
|
||||
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Notesnook.API/Extensions/ClaimsPrincipalExtensions.cs
Normal file
14
Notesnook.API/Extensions/ClaimsPrincipalExtensions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Threading;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
|
||||
namespace System.Security.Claims
|
||||
{
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
private readonly static string[] SUBSCRIBED_CLAIMS = ["believer", "education", "essential", "pro", "premium", "premium_canceled"];
|
||||
public static bool IsUserSubscribed(this ClaimsPrincipal user)
|
||||
=> user.Claims.Any((c) => c.Type == "notesnook:status" && SUBSCRIBED_CLAIMS.Contains(c.Value));
|
||||
}
|
||||
}
|
||||
68
Notesnook.API/Helpers/StorageHelper.cs
Normal file
68
Notesnook.API/Helpers/StorageHelper.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Notesnook.API.Models;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Notesnook.API.Helpers
|
||||
{
|
||||
class StorageHelper
|
||||
{
|
||||
const long MB = 1024 * 1024;
|
||||
const long GB = 1024 * MB;
|
||||
public readonly static Dictionary<SubscriptionPlan, long> MAX_STORAGE_PER_MONTH = new()
|
||||
{
|
||||
{ SubscriptionPlan.FREE, 50L * MB },
|
||||
{ SubscriptionPlan.ESSENTIAL, GB },
|
||||
{ SubscriptionPlan.PRO, 10L * GB },
|
||||
{ SubscriptionPlan.EDUCATION, 10L * GB },
|
||||
{ SubscriptionPlan.BELIEVER, 25L * GB },
|
||||
{ SubscriptionPlan.LEGACY_PRO, -1 }
|
||||
};
|
||||
public readonly static Dictionary<SubscriptionPlan, long> MAX_FILE_SIZE = new()
|
||||
{
|
||||
{ SubscriptionPlan.FREE, 10 * MB },
|
||||
{ SubscriptionPlan.ESSENTIAL, 100 * MB },
|
||||
{ SubscriptionPlan.PRO, 1L * GB },
|
||||
{ SubscriptionPlan.EDUCATION, 1L * GB },
|
||||
{ SubscriptionPlan.BELIEVER, 5L * GB },
|
||||
{ SubscriptionPlan.LEGACY_PRO, 512 * MB }
|
||||
};
|
||||
|
||||
public static long GetStorageLimitForPlan(Subscription subscription)
|
||||
{
|
||||
return MAX_STORAGE_PER_MONTH[subscription.Plan];
|
||||
}
|
||||
|
||||
public static long GetFileSizeLimitForPlan(Subscription subscription)
|
||||
{
|
||||
return MAX_FILE_SIZE[subscription.Plan];
|
||||
}
|
||||
|
||||
public static bool IsStorageLimitReached(Subscription subscription, Limit limit)
|
||||
{
|
||||
var storageLimit = GetStorageLimitForPlan(subscription);
|
||||
if (storageLimit == -1) return false;
|
||||
return limit.Value > storageLimit;
|
||||
}
|
||||
|
||||
public static bool IsFileSizeExceeded(Subscription subscription, long fileSize)
|
||||
{
|
||||
var maxFileSize = MAX_FILE_SIZE[subscription.Plan];
|
||||
return fileSize > maxFileSize;
|
||||
}
|
||||
|
||||
private static readonly string[] sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
public static string FormatBytes(long size)
|
||||
{
|
||||
int order = 0;
|
||||
while (size >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
size = size / 1024;
|
||||
}
|
||||
|
||||
return String.Format("{0:0.##} {1}", size, sizes[order]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
@@ -40,16 +42,16 @@ namespace Notesnook.API.Hubs
|
||||
{
|
||||
Task<bool> SendItems(SyncTransferItemV2 transferItem);
|
||||
Task<bool> SendVaultKey(EncryptedData vaultKey);
|
||||
Task<bool> SendMonographs(IEnumerable<Monograph> monographs);
|
||||
Task<bool> SendMonographs(IEnumerable<MonographMetadata> monographs);
|
||||
Task PushCompleted();
|
||||
}
|
||||
|
||||
[Authorize("Sync")]
|
||||
[Authorize]
|
||||
public class SyncV2Hub : Hub<ISyncV2HubClient>
|
||||
{
|
||||
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||
private readonly IUnitOfWork unit;
|
||||
private readonly string[] CollectionKeys = [
|
||||
private static readonly string[] CollectionKeys = [
|
||||
"settingitem",
|
||||
"attachment",
|
||||
"note",
|
||||
@@ -62,11 +64,40 @@ namespace Notesnook.API.Hubs
|
||||
"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, string[], bool, int, Task<IAsyncCursor<SyncItem>>>[] Collections;
|
||||
|
||||
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork)
|
||||
{
|
||||
Repositories = syncItemsRepositoryAccessor;
|
||||
unit = unitOfWork;
|
||||
|
||||
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,
|
||||
];
|
||||
UpsertActionsMap = new Dictionary<string, Action<IEnumerable<SyncItem>, string, long>> {
|
||||
{ "settingitem", Repositories.Settings.UpsertMany },
|
||||
{ "attachment", Repositories.Attachments.UpsertMany },
|
||||
{ "note", Repositories.Notes.UpsertMany },
|
||||
{ "notebook", Repositories.Notebooks.UpsertMany },
|
||||
{ "content", Repositories.Contents.UpsertMany },
|
||||
{ "shortcut", Repositories.Shortcuts.UpsertMany },
|
||||
{ "reminder", Repositories.Reminders.UpsertMany },
|
||||
{ "relation", Repositories.Relations.UpsertMany },
|
||||
{ "color", Repositories.Colors.UpsertMany },
|
||||
{ "vault", Repositories.Vaults.UpsertMany },
|
||||
{ "tag", Repositories.Tags.UpsertMany },
|
||||
}.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
@@ -74,65 +105,26 @@ namespace Notesnook.API.Hubs
|
||||
var result = new SyncRequirement().IsAuthorized(Context.User, new PathString("/hubs/sync/v2"));
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
var reason = result.AuthorizationFailure.FailureReasons.FirstOrDefault();
|
||||
var reason = result.AuthorizationFailure?.FailureReasons.FirstOrDefault();
|
||||
throw new HubException(reason?.Message ?? "Unauthorized");
|
||||
}
|
||||
var id = Context.User.FindFirstValue("sub");
|
||||
var id = Context.User?.FindFirstValue("sub") ?? throw new HubException("User not found.");
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, id);
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
private Action<IEnumerable<SyncItem>, string, long> MapTypeToUpsertAction(string type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"settingitem" => Repositories.Settings.UpsertMany,
|
||||
"attachment" => Repositories.Attachments.UpsertMany,
|
||||
"note" => Repositories.Notes.UpsertMany,
|
||||
"notebook" => Repositories.Notebooks.UpsertMany,
|
||||
"content" => Repositories.Contents.UpsertMany,
|
||||
"shortcut" => Repositories.Shortcuts.UpsertMany,
|
||||
"reminder" => Repositories.Reminders.UpsertMany,
|
||||
"relation" => Repositories.Relations.UpsertMany,
|
||||
"color" => Repositories.Colors.UpsertMany,
|
||||
"vault" => Repositories.Vaults.UpsertMany,
|
||||
"tag" => Repositories.Tags.UpsertMany,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>> 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<int> PushItems(string deviceId, SyncTransferItemV2 pushItem)
|
||||
{
|
||||
var userId = Context.User.FindFirstValue("sub");
|
||||
if (string.IsNullOrEmpty(userId)) throw new HubException("Please login to sync.");
|
||||
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync.");
|
||||
|
||||
SyncEventCounterSource.Log.PushV2();
|
||||
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
|
||||
var UpsertItems = MapTypeToUpsertAction(pushItem.Type) ?? throw new Exception($"Invalid item type: {pushItem.Type}.");
|
||||
var UpsertItems = UpsertActionsMap[pushItem.Type] ?? throw new Exception($"Invalid item type: {pushItem.Type}.");
|
||||
UpsertItems(pushItem.Items, userId, 1);
|
||||
|
||||
if (!await unit.Commit()) return 0;
|
||||
@@ -142,29 +134,28 @@ namespace Notesnook.API.Hubs
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
SyncEventCounterSource.Log.RecordPushDuration(stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PushCompleted()
|
||||
{
|
||||
var userId = Context.User.FindFirstValue("sub");
|
||||
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("User not found.");
|
||||
await Clients.OthersInGroup(userId).PushCompleted();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(Func<string, string[], bool, int, Task<IAsyncCursor<SyncItem>>>[] collections, string[] types, string userId, string[] ids, int size, bool resetSync, long maxBytes)
|
||||
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, string[] ids, int size, bool resetSync, long maxBytes)
|
||||
{
|
||||
var itemsProcessed = 0;
|
||||
for (int i = 0; i < collections.Length; i++)
|
||||
for (int i = 0; i < Collections.Length; i++)
|
||||
{
|
||||
var type = types[i];
|
||||
var type = CollectionKeys[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);
|
||||
using var cursor = await Collections[i](userId, filteredIds, resetSync, size);
|
||||
|
||||
var chunk = new List<SyncItem>();
|
||||
long totalBytes = 0;
|
||||
@@ -206,8 +197,17 @@ namespace Notesnook.API.Hubs
|
||||
|
||||
public async Task<SyncV2Metadata> RequestFetch(string deviceId)
|
||||
{
|
||||
var userId = Context.User.FindFirstValue("sub");
|
||||
if (string.IsNullOrEmpty(userId)) throw new HubException("Please login to sync.");
|
||||
return await HandleRequestFetch(deviceId, false);
|
||||
}
|
||||
|
||||
public async Task<SyncV2Metadata> RequestFetchV2(string deviceId)
|
||||
{
|
||||
return await HandleRequestFetch(deviceId, true);
|
||||
}
|
||||
|
||||
private async Task<SyncV2Metadata> HandleRequestFetch(string deviceId, bool includeMonographs)
|
||||
{
|
||||
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync.");
|
||||
|
||||
SyncEventCounterSource.Log.FetchV2();
|
||||
|
||||
@@ -223,27 +223,12 @@ namespace Notesnook.API.Hubs
|
||||
!isResetSync)
|
||||
return new SyncV2Metadata { Synced = true };
|
||||
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
string[] ids = deviceService.FetchUnsyncedIds();
|
||||
|
||||
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,
|
||||
@@ -270,14 +255,35 @@ namespace Notesnook.API.Hubs
|
||||
}
|
||||
}
|
||||
|
||||
var unsyncedMonographs = ids.Where((id) => id.EndsWith(":monograph")).ToHashSet();
|
||||
var unsyncedMonographIds = unsyncedMonographs.Select((id) => id.Split(":")[0]).ToArray();
|
||||
var userMonographs = isResetSync
|
||||
? await Repositories.Monographs.FindAsync(m => m.UserId == userId)
|
||||
: await Repositories.Monographs.FindAsync(m => m.UserId == userId && unsyncedMonographIds.Contains(m.ItemId));
|
||||
if (includeMonographs)
|
||||
{
|
||||
var isSyncingMonographsForFirstTime = !device.HasInitialMonographsSync;
|
||||
var unsyncedMonographs = ids.Where((id) => id.EndsWith(":monograph")).ToHashSet();
|
||||
var unsyncedMonographIds = unsyncedMonographs.Select((id) => id.Split(":")[0]).ToArray();
|
||||
FilterDefinition<Monograph> filter = isResetSync || isSyncingMonographsForFirstTime
|
||||
? Builders<Monograph>.Filter.Eq("UserId", userId)
|
||||
: Builders<Monograph>.Filter.And(
|
||||
Builders<Monograph>.Filter.Eq("UserId", userId),
|
||||
Builders<Monograph>.Filter.Or(
|
||||
Builders<Monograph>.Filter.In("ItemId", unsyncedMonographIds),
|
||||
Builders<Monograph>.Filter.In("_id", unsyncedMonographIds)
|
||||
)
|
||||
);
|
||||
var userMonographs = await Repositories.Monographs.Collection.Find(filter).Project((m) => new MonographMetadata
|
||||
{
|
||||
DatePublished = m.DatePublished,
|
||||
Deleted = m.Deleted,
|
||||
Password = m.Password,
|
||||
SelfDestruct = m.SelfDestruct,
|
||||
Title = m.Title,
|
||||
ItemId = m.ItemId ?? m.Id.ToString(),
|
||||
}).ToListAsync();
|
||||
|
||||
if (userMonographs.Any() && !await Clients.Caller.SendMonographs(userMonographs).WaitAsync(TimeSpan.FromMinutes(10)))
|
||||
throw new HubException("Client rejected monographs.");
|
||||
if (userMonographs.Count > 0 && !await Clients.Caller.SendMonographs(userMonographs).WaitAsync(TimeSpan.FromMinutes(10)))
|
||||
throw new HubException("Client rejected monographs.");
|
||||
|
||||
device.HasInitialMonographsSync = true;
|
||||
}
|
||||
|
||||
deviceService.Reset();
|
||||
|
||||
@@ -288,7 +294,6 @@ namespace Notesnook.API.Hubs
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
SyncEventCounterSource.Log.RecordFetchDuration(stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
@@ -301,24 +306,4 @@ namespace Notesnook.API.Hubs
|
||||
[JsonPropertyName("synced")]
|
||||
public bool Synced { get; set; }
|
||||
}
|
||||
|
||||
[MessagePack.MessagePackObject]
|
||||
public struct SyncV2TransferItem
|
||||
{
|
||||
[MessagePack.Key("items")]
|
||||
[JsonPropertyName("items")]
|
||||
public IEnumerable<SyncItem> 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; }
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,40 @@
|
||||
/*
|
||||
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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.S3.Model;
|
||||
using Notesnook.API.Models;
|
||||
using Notesnook.API.Models.Responses;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
|
||||
namespace Notesnook.API.Interfaces
|
||||
{
|
||||
public interface IS3Service
|
||||
{
|
||||
Task DeleteObjectAsync(string userId, string name);
|
||||
Task DeleteDirectoryAsync(string userId);
|
||||
Task<long> GetObjectSizeAsync(string userId, string name);
|
||||
string GetUploadObjectUrl(string userId, string name);
|
||||
string GetDownloadObjectUrl(string userId, string name);
|
||||
Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string uploadId = null);
|
||||
Task AbortMultipartUploadAsync(string userId, string name, string uploadId);
|
||||
Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest);
|
||||
}
|
||||
/*
|
||||
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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.S3.Model;
|
||||
using Notesnook.API.Models;
|
||||
using Notesnook.API.Models.Responses;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
|
||||
namespace Notesnook.API.Interfaces
|
||||
{
|
||||
public interface IS3Service
|
||||
{
|
||||
Task DeleteObjectAsync(string userId, string name);
|
||||
Task DeleteDirectoryAsync(string userId);
|
||||
Task<long> GetObjectSizeAsync(string userId, string name);
|
||||
string? GetUploadObjectUrl(string userId, string name);
|
||||
Task<string?> GetDownloadObjectUrl(string userId, string name);
|
||||
Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string? uploadId = null);
|
||||
Task AbortMultipartUploadAsync(string userId, string name, string uploadId);
|
||||
Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,9 @@ 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; }
|
||||
}
|
||||
}
|
||||
64
Notesnook.API/Jobs/DeviceCleanupJob.cs
Normal file
64
Notesnook.API/Jobs/DeviceCleanupJob.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Quartz;
|
||||
|
||||
namespace Notesnook.API.Jobs
|
||||
{
|
||||
public class DeviceCleanupJob : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
ParallelOptions parallelOptions = new()
|
||||
{
|
||||
MaxDegreeOfParallelism = 100,
|
||||
CancellationToken = context.CancellationToken,
|
||||
};
|
||||
Parallel.ForEach(Directory.EnumerateDirectories("sync"), parallelOptions, (userDir, ct) =>
|
||||
{
|
||||
foreach (var device in Directory.EnumerateDirectories(userDir))
|
||||
{
|
||||
string lastAccessFile = Path.Combine(device, "LastAccessTime");
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(lastAccessFile))
|
||||
{
|
||||
Directory.Delete(device, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
string content = File.ReadAllText(lastAccessFile);
|
||||
if (!long.TryParse(content, out long lastAccessTime) || lastAccessTime <= 0)
|
||||
{
|
||||
Directory.Delete(device, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
DateTimeOffset accessTime;
|
||||
try
|
||||
{
|
||||
accessTime = DateTimeOffset.FromUnixTimeMilliseconds(lastAccessTime);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Directory.Delete(device, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the device hasn't been accessed for more than one month, delete it.
|
||||
if (accessTime.AddMonths(1) < DateTimeOffset.UtcNow)
|
||||
{
|
||||
Directory.Delete(device, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the error and continue processing other directories.
|
||||
Console.Error.WriteLine($"Error processing device '{device}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
60
Notesnook.API/Models/InboxApiKey.cs
Normal file
60
Notesnook.API/Models/InboxApiKey.cs
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,26 +69,26 @@ namespace Notesnook.API.Models
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonPropertyName("userId")]
|
||||
public string UserId { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("selfDestruct")]
|
||||
public bool SelfDestruct { get; set; }
|
||||
|
||||
[JsonPropertyName("encryptedContent")]
|
||||
public EncryptedData EncryptedContent { get; set; }
|
||||
public EncryptedData? EncryptedContent { get; set; }
|
||||
|
||||
[JsonPropertyName("datePublished")]
|
||||
public long DatePublished { get; set; }
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
[BsonIgnore]
|
||||
public string Content { get; set; }
|
||||
public string? Content { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public byte[] CompressedContent { get; set; }
|
||||
public byte[]? CompressedContent { get; set; }
|
||||
|
||||
[JsonPropertyName("password")]
|
||||
public EncryptedData Password { get; set; }
|
||||
public EncryptedData? Password { get; set; }
|
||||
|
||||
[JsonPropertyName("deleted")]
|
||||
public bool Deleted { get; set; }
|
||||
|
||||
@@ -17,25 +17,19 @@ 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.Text.Json.Serialization;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using System.Runtime.Serialization;
|
||||
using Streetwriters.Common.Attributes;
|
||||
using Streetwriters.Common.Converters;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Streetwriters.Common.Interfaces
|
||||
namespace Notesnook.API.Models
|
||||
{
|
||||
[JsonInterfaceConverter(typeof(InterfaceConverter<ISubscription, Subscription>))]
|
||||
public interface ISubscription : IDocument
|
||||
|
||||
public class MonographContent
|
||||
{
|
||||
string UserId { get; set; }
|
||||
ApplicationType AppId { get; set; }
|
||||
SubscriptionProvider Provider { get; set; }
|
||||
long StartDate { get; set; }
|
||||
long ExpiryDate { get; set; }
|
||||
SubscriptionType Type { get; set; }
|
||||
string OrderId { get; set; }
|
||||
string SubscriptionId { get; set; }
|
||||
[JsonPropertyName("data")]
|
||||
public string Data { get; set; }
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Notesnook.API/Models/MonographMetadata.cs
Normal file
52
Notesnook.API/Models/MonographMetadata.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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.Text.Json.Serialization;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Notesnook.API.Models
|
||||
{
|
||||
public class MonographMetadata
|
||||
{
|
||||
[DataMember(Name = "id")]
|
||||
[JsonPropertyName("id")]
|
||||
[MessagePack.Key("id")]
|
||||
public required string ItemId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public required string Title { get; set; }
|
||||
|
||||
[JsonPropertyName("selfDestruct")]
|
||||
public bool SelfDestruct { get; set; }
|
||||
|
||||
[JsonPropertyName("datePublished")]
|
||||
public long DatePublished { get; set; }
|
||||
|
||||
[JsonPropertyName("password")]
|
||||
public EncryptedData? Password { get; set; }
|
||||
|
||||
[JsonPropertyName("deleted")]
|
||||
public bool Deleted { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Net.Http;
|
||||
using System.Text.Json.Serialization;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Models;
|
||||
@@ -7,22 +8,30 @@ namespace Notesnook.API.Models.Responses
|
||||
public class UserResponse : UserModel, IResponse
|
||||
{
|
||||
[JsonPropertyName("salt")]
|
||||
public string Salt { get; set; }
|
||||
public string? Salt { get; set; }
|
||||
|
||||
[JsonPropertyName("attachmentsKey")]
|
||||
public EncryptedData AttachmentsKey { get; set; }
|
||||
public EncryptedData? AttachmentsKey { get; set; }
|
||||
|
||||
[JsonPropertyName("monographPasswordsKey")]
|
||||
public EncryptedData MonographPasswordsKey { get; set; }
|
||||
public EncryptedData? MonographPasswordsKey { get; set; }
|
||||
|
||||
[JsonPropertyName("inboxKeys")]
|
||||
public InboxKeys? InboxKeys { get; set; }
|
||||
|
||||
[JsonPropertyName("subscription")]
|
||||
public ISubscription Subscription { get; set; }
|
||||
public Subscription? Subscription { get; set; }
|
||||
|
||||
[JsonPropertyName("profile")]
|
||||
public EncryptedData Profile { get; set; }
|
||||
[JsonPropertyName("storageUsed")]
|
||||
public long StorageUsed { get; set; }
|
||||
|
||||
[JsonPropertyName("totalStorage")]
|
||||
public long TotalStorage { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool Success { get; set; }
|
||||
public int StatusCode { get; set; }
|
||||
[JsonIgnore]
|
||||
public HttpContent? Content { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Notesnook.API.Models
|
||||
public string Algorithm
|
||||
{
|
||||
get; set;
|
||||
} = Algorithms.Default;
|
||||
}
|
||||
}
|
||||
|
||||
public class SyncItemBsonSerializer : SerializerBase<SyncItem>
|
||||
|
||||
@@ -19,9 +19,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace Notesnook.API.Models
|
||||
{
|
||||
public class UserKeys
|
||||
{
|
||||
public EncryptedData AttachmentsKey { get; set; }
|
||||
public EncryptedData MonographPasswordsKey { get; set; }
|
||||
}
|
||||
public class UserKeys
|
||||
{
|
||||
public EncryptedData? AttachmentsKey { get; set; }
|
||||
public EncryptedData? MonographPasswordsKey { get; set; }
|
||||
public InboxKeys? InboxKeys { get; set; }
|
||||
}
|
||||
|
||||
public class InboxKeys
|
||||
{
|
||||
public string? Public { get; set; }
|
||||
public EncryptedData? Private { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ using Notesnook.API.Interfaces;
|
||||
|
||||
namespace Notesnook.API.Models
|
||||
{
|
||||
public class Limit
|
||||
{
|
||||
public long Value { get; set; }
|
||||
public long UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class UserSettings : IUserSettings
|
||||
{
|
||||
public UserSettings()
|
||||
@@ -32,9 +38,11 @@ namespace Notesnook.API.Models
|
||||
public string UserId { get; set; }
|
||||
public long LastSynced { get; set; }
|
||||
public string Salt { get; set; }
|
||||
public EncryptedData VaultKey { get; set; }
|
||||
public EncryptedData AttachmentsKey { get; set; }
|
||||
public EncryptedData MonographPasswordsKey { get; set; }
|
||||
public EncryptedData? VaultKey { get; set; }
|
||||
public EncryptedData? AttachmentsKey { get; set; }
|
||||
public EncryptedData? MonographPasswordsKey { get; set; }
|
||||
public InboxKeys? InboxKeys { get; set; }
|
||||
public Limit StorageLimit { get; set; }
|
||||
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<StartupObject>Notesnook.API.Program</StartupObject>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.3.0" />
|
||||
<PackageReference Include="AWSSDK.Core" Version="3.7.304.31" />
|
||||
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
||||
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="6.2.0" />
|
||||
@@ -15,8 +17,11 @@
|
||||
<PackageReference Include="AWSSDK.S3" Version="3.7.310.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.2.0" />
|
||||
<PackageReference Include="Nanoid" Version="3.1.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-alpha.2" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
|
||||
<PackageReference Include="Quartz" Version="3.5.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
@@ -114,6 +114,9 @@ namespace Notesnook.API.Repositories
|
||||
throw new Exception($"Corrupted item \"{item.ItemId}\" in collection \"{this.collectionName}\". Please report this error to support@streetwriters.co.");
|
||||
}
|
||||
|
||||
if (item.ItemId == null)
|
||||
throw new Exception($"Item does not have an ItemId.");
|
||||
|
||||
item.DateSynced = dateSynced;
|
||||
item.UserId = userId;
|
||||
|
||||
@@ -148,6 +151,9 @@ namespace Notesnook.API.Repositories
|
||||
throw new Exception($"Corrupted item \"{item.ItemId}\" in collection \"{this.collectionName}\". Please report this error to support@streetwriters.co.");
|
||||
}
|
||||
|
||||
if (item.ItemId == null)
|
||||
throw new Exception($"Item does not have an ItemId.");
|
||||
|
||||
var filter = Builders<SyncItem>.Filter.And(
|
||||
userIdFilter,
|
||||
Builders<SyncItem>.Filter.Eq("ItemId", item.ItemId)
|
||||
|
||||
@@ -28,9 +28,13 @@ using Amazon.Runtime;
|
||||
using Amazon.S3;
|
||||
using Amazon.S3.Model;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Notesnook.API.Helpers;
|
||||
using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Notesnook.API.Services
|
||||
{
|
||||
@@ -45,6 +49,7 @@ namespace Notesnook.API.Services
|
||||
private readonly string BUCKET_NAME = Constants.S3_BUCKET_NAME ?? "";
|
||||
private readonly string INTERNAL_BUCKET_NAME = Constants.S3_INTERNAL_BUCKET_NAME ?? "";
|
||||
private AmazonS3Client S3Client { get; }
|
||||
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||
|
||||
// When running in a dockerized environment the sync server doesn't have access
|
||||
// to the host's S3 Service URL. It can only talk to S3 server via its own internal
|
||||
@@ -57,8 +62,9 @@ namespace Notesnook.API.Services
|
||||
private AmazonS3Client S3InternalClient { get; }
|
||||
private HttpClient httpClient = new HttpClient();
|
||||
|
||||
public S3Service()
|
||||
public S3Service(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor)
|
||||
{
|
||||
Repositories = syncItemsRepositoryAccessor;
|
||||
var config = new AmazonS3Config
|
||||
{
|
||||
#if (DEBUG || STAGING)
|
||||
@@ -145,31 +151,33 @@ namespace Notesnook.API.Services
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Head, url);
|
||||
var response = await httpClient.SendAsync(request);
|
||||
const long MAX_SIZE = 513 * 1024 * 1024; // 512 MB
|
||||
if (!Constants.IS_SELF_HOSTED && response.Content.Headers.ContentLength >= MAX_SIZE)
|
||||
{
|
||||
await this.DeleteObjectAsync(userId, name);
|
||||
throw new Exception("File size exceeds the maximum allowed size.");
|
||||
}
|
||||
return response.Content.Headers.ContentLength ?? 0;
|
||||
}
|
||||
|
||||
|
||||
public string GetUploadObjectUrl(string userId, string name)
|
||||
public string? GetUploadObjectUrl(string userId, string name)
|
||||
{
|
||||
var url = this.GetPresignedURL(userId, name, HttpVerb.PUT);
|
||||
if (url == null) return null;
|
||||
return url;
|
||||
return this.GetPresignedURL(userId, name, HttpVerb.PUT);
|
||||
}
|
||||
|
||||
public string GetDownloadObjectUrl(string userId, string name)
|
||||
public async Task<string?> GetDownloadObjectUrl(string userId, string name)
|
||||
{
|
||||
// var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
|
||||
// var subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId);
|
||||
|
||||
// var size = await GetObjectSizeAsync(userId, name);
|
||||
// if (StorageHelper.IsFileSizeExceeded(subscription, size))
|
||||
// {
|
||||
// var fileSizeLimit = StorageHelper.GetFileSizeLimitForPlan(subscription);
|
||||
// throw new Exception($"You cannot download files larger than {StorageHelper.FormatBytes(fileSizeLimit)} on this plan.");
|
||||
// }
|
||||
|
||||
var url = this.GetPresignedURL(userId, name, HttpVerb.GET);
|
||||
if (url == null) return null;
|
||||
return url;
|
||||
}
|
||||
|
||||
public async Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string uploadId = null)
|
||||
public async Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string? uploadId = null)
|
||||
{
|
||||
var objectName = GetFullObjectName(userId, name);
|
||||
if (userId == null || objectName == null) throw new Exception("Could not initiate multipart upload.");
|
||||
@@ -204,18 +212,58 @@ namespace Notesnook.API.Services
|
||||
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to abort multipart upload.");
|
||||
}
|
||||
|
||||
private async Task<long> GetMultipartUploadSizeAsync(string userId, string key, string uploadId)
|
||||
{
|
||||
var objectName = GetFullObjectName(userId, key);
|
||||
var parts = await GetS3Client(S3ClientMode.INTERNAL).ListPartsAsync(GetBucketName(S3ClientMode.INTERNAL), objectName, uploadId);
|
||||
long totalSize = 0;
|
||||
foreach (var part in parts.Parts)
|
||||
{
|
||||
totalSize += part.Size;
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
public async Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest)
|
||||
{
|
||||
var objectName = GetFullObjectName(userId, uploadRequest.Key);
|
||||
if (userId == null || objectName == null) throw new Exception("Could not abort multipart upload.");
|
||||
|
||||
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
|
||||
var subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
|
||||
|
||||
long fileSize = await GetMultipartUploadSizeAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
|
||||
if (StorageHelper.IsFileSizeExceeded(subscription, fileSize))
|
||||
{
|
||||
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
|
||||
throw new Exception("Max file size exceeded.");
|
||||
}
|
||||
|
||||
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||
if (userSettings == null)
|
||||
{
|
||||
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
|
||||
throw new Exception("User settings not found.");
|
||||
}
|
||||
|
||||
userSettings.StorageLimit ??= new Limit { Value = 0, UpdatedAt = 0 };
|
||||
userSettings.StorageLimit.Value += fileSize;
|
||||
if (StorageHelper.IsStorageLimitReached(subscription, userSettings.StorageLimit))
|
||||
{
|
||||
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
|
||||
throw new Exception("Storage limit reached.");
|
||||
}
|
||||
|
||||
uploadRequest.Key = objectName;
|
||||
uploadRequest.BucketName = GetBucketName(S3ClientMode.INTERNAL);
|
||||
var response = await GetS3Client(S3ClientMode.INTERNAL).CompleteMultipartUploadAsync(uploadRequest);
|
||||
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to complete multipart upload.");
|
||||
|
||||
userSettings.StorageLimit.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
|
||||
}
|
||||
|
||||
private string GetPresignedURL(string userId, string name, HttpVerb httpVerb, S3ClientMode mode = S3ClientMode.EXTERNAL)
|
||||
private string? GetPresignedURL(string userId, string name, HttpVerb httpVerb, S3ClientMode mode = S3ClientMode.EXTERNAL)
|
||||
{
|
||||
var objectName = GetFullObjectName(userId, name);
|
||||
if (userId == null || objectName == null) return null;
|
||||
|
||||
@@ -44,6 +44,16 @@ namespace Notesnook.API.Services
|
||||
set => SetMetadata("LastAccessTime", value.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the monographs have been synced for the first time
|
||||
/// ever on a device.
|
||||
/// </summary>
|
||||
public readonly bool HasInitialMonographsSync
|
||||
{
|
||||
get => !string.IsNullOrEmpty(GetMetadata("HasInitialMonographsSync"));
|
||||
set => SetMetadata("HasInitialMonographsSync", value.ToString());
|
||||
}
|
||||
|
||||
private static string CreateFilePath(string userId, string? deviceId = null, string? metadataKey = null)
|
||||
{
|
||||
return Path.Join("sync", userId, deviceId, metadataKey);
|
||||
|
||||
@@ -18,13 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Notesnook.API.Helpers;
|
||||
using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
using Notesnook.API.Models.Responses;
|
||||
@@ -73,19 +72,22 @@ namespace Notesnook.API.Services
|
||||
await Repositories.UsersSettings.InsertAsync(new UserSettings
|
||||
{
|
||||
UserId = response.UserId,
|
||||
StorageLimit = new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 },
|
||||
LastSynced = 0,
|
||||
Salt = GetSalt()
|
||||
});
|
||||
|
||||
if (!Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.CreateSubscriptionTopic, new CreateSubscriptionMessage
|
||||
await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.CreateSubscriptionV2Topic, new CreateSubscriptionMessageV2
|
||||
{
|
||||
AppId = ApplicationType.NOTESNOOK,
|
||||
Provider = SubscriptionProvider.STREETWRITERS,
|
||||
Type = SubscriptionType.BASIC,
|
||||
Status = SubscriptionStatus.ACTIVE,
|
||||
Plan = SubscriptionPlan.FREE,
|
||||
UserId = response.UserId,
|
||||
StartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,14 +100,15 @@ namespace Notesnook.API.Services
|
||||
|
||||
var user = await userService.GetUserAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User not found.");
|
||||
|
||||
ISubscription subscription = null;
|
||||
Subscription? subscription = null;
|
||||
if (Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
subscription = new Subscription
|
||||
{
|
||||
AppId = ApplicationType.NOTESNOOK,
|
||||
Provider = SubscriptionProvider.STREETWRITERS,
|
||||
Type = SubscriptionType.PREMIUM,
|
||||
Plan = SubscriptionPlan.BELIEVER,
|
||||
Status = SubscriptionStatus.ACTIVE,
|
||||
UserId = user.UserId,
|
||||
StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
// this date doesn't matter as the subscription is static.
|
||||
@@ -115,10 +118,18 @@ namespace Notesnook.API.Services
|
||||
else
|
||||
{
|
||||
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
|
||||
subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId);
|
||||
subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
|
||||
}
|
||||
|
||||
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == user.UserId) ?? throw new Exception("User settings not found.");
|
||||
|
||||
// reset user's attachment limit every month
|
||||
if (userSettings.StorageLimit == null || DateTimeOffset.UtcNow.Month > DateTimeOffset.FromUnixTimeMilliseconds(userSettings.StorageLimit.UpdatedAt).Month)
|
||||
{
|
||||
userSettings.StorageLimit ??= new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 };
|
||||
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == user.UserId);
|
||||
}
|
||||
|
||||
return new UserResponse
|
||||
{
|
||||
UserId = user.UserId,
|
||||
@@ -129,8 +140,11 @@ namespace Notesnook.API.Services
|
||||
PhoneNumber = user.PhoneNumber,
|
||||
AttachmentsKey = userSettings.AttachmentsKey,
|
||||
MonographPasswordsKey = userSettings.MonographPasswordsKey,
|
||||
InboxKeys = userSettings.InboxKeys,
|
||||
Salt = userSettings.Salt,
|
||||
Subscription = subscription,
|
||||
StorageUsed = userSettings.StorageLimit.Value,
|
||||
TotalStorage = StorageHelper.GetStorageLimitForPlan(subscription),
|
||||
Success = true,
|
||||
StatusCode = 200
|
||||
};
|
||||
@@ -148,6 +162,27 @@ namespace Notesnook.API.Services
|
||||
{
|
||||
userSettings.MonographPasswordsKey = keys.MonographPasswordsKey;
|
||||
}
|
||||
if (keys.InboxKeys != null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
|
||||
}
|
||||
@@ -172,6 +207,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<UserService>.Info(nameof(DeleteUserAsync), "User data deleted", userId, result.ToString());
|
||||
@@ -231,6 +267,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);
|
||||
@@ -238,6 +275,7 @@ namespace Notesnook.API.Services
|
||||
userSettings.AttachmentsKey = null;
|
||||
userSettings.MonographPasswordsKey = null;
|
||||
userSettings.VaultKey = null;
|
||||
userSettings.InboxKeys = null;
|
||||
userSettings.LastSynced = 0;
|
||||
|
||||
await Repositories.UsersSettings.UpsertAsync(userSettings, (s) => s.UserId == userId);
|
||||
|
||||
@@ -48,15 +48,19 @@ using Notesnook.API.Authorization;
|
||||
using Notesnook.API.Extensions;
|
||||
using Notesnook.API.Hubs;
|
||||
using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Jobs;
|
||||
using Notesnook.API.Models;
|
||||
using Notesnook.API.Repositories;
|
||||
using Notesnook.API.Services;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Resources;
|
||||
using Quartz;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Extensions;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Common.Services;
|
||||
using Streetwriters.Data;
|
||||
using Streetwriters.Data.DbContexts;
|
||||
using Streetwriters.Data.Interfaces;
|
||||
@@ -108,12 +112,11 @@ namespace Notesnook.API
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.Requirements.Add(new SyncRequirement());
|
||||
});
|
||||
options.AddPolicy("Pro", policy =>
|
||||
|
||||
options.AddPolicy(InboxApiKeyAuthenticationDefaults.AuthenticationScheme, policy =>
|
||||
{
|
||||
policy.AuthenticationSchemes.Add("introspection");
|
||||
policy.AuthenticationSchemes.Add(InboxApiKeyAuthenticationDefaults.AuthenticationScheme);
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.Requirements.Add(new SyncRequirement());
|
||||
policy.Requirements.Add(new ProUserRequirement());
|
||||
});
|
||||
|
||||
options.DefaultPolicy = options.GetPolicy("Notesnook");
|
||||
@@ -148,7 +151,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)))
|
||||
@@ -165,7 +172,8 @@ namespace Notesnook.API
|
||||
|
||||
services.AddRepository<UserSettings>("user_settings", "notesnook")
|
||||
.AddRepository<Monograph>("monographs", "notesnook")
|
||||
.AddRepository<Announcement>("announcements", "notesnook");
|
||||
.AddRepository<Announcement>("announcements", "notesnook")
|
||||
.AddRepository<InboxApiKey>(Collections.InboxApiKeysKey, "notesnook");
|
||||
|
||||
services.AddMongoCollection(Collections.SettingsKey)
|
||||
.AddMongoCollection(Collections.AttachmentsKey)
|
||||
@@ -178,11 +186,14 @@ namespace Notesnook.API
|
||||
.AddMongoCollection(Collections.ShortcutsKey)
|
||||
.AddMongoCollection(Collections.TagsKey)
|
||||
.AddMongoCollection(Collections.ColorsKey)
|
||||
.AddMongoCollection(Collections.VaultsKey);
|
||||
.AddMongoCollection(Collections.VaultsKey)
|
||||
.AddMongoCollection(Collections.InboxItems)
|
||||
.AddMongoCollection(Collections.InboxApiKeysKey);
|
||||
|
||||
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
services.AddScoped<IS3Service, S3Service>();
|
||||
services.AddScoped<IURLAnalyzer, URLAnalyzer>();
|
||||
|
||||
services.AddControllers();
|
||||
|
||||
@@ -216,6 +227,24 @@ namespace Notesnook.API
|
||||
.WithMetrics((builder) => builder
|
||||
.AddMeter("Notesnook.API.Metrics.Sync")
|
||||
.AddPrometheusExporter());
|
||||
|
||||
services.AddQuartzHostedService(q =>
|
||||
{
|
||||
q.WaitForJobsToComplete = false;
|
||||
q.AwaitApplicationStarted = true;
|
||||
q.StartDelay = TimeSpan.FromMinutes(1);
|
||||
}).AddQuartz(q =>
|
||||
{
|
||||
q.UseMicrosoftDependencyInjectionJobFactory();
|
||||
|
||||
var jobKey = new JobKey("DeviceCleanupJob");
|
||||
q.AddJob<DeviceCleanupJob>(opts => opts.WithIdentity(jobKey));
|
||||
q.AddTrigger(opts => opts
|
||||
.ForJob(jobKey)
|
||||
.WithIdentity("DeviceCleanup-trigger")
|
||||
// first of every month
|
||||
.WithCronSchedule("0 0 0 1 * ? *"));
|
||||
});
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
@@ -257,7 +286,6 @@ namespace Notesnook.API
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapPrometheusScrapingEndpoint();
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapHealthChecks("/health");
|
||||
endpoints.MapHub<SyncHub>("/hubs/sync", options =>
|
||||
|
||||
2
Notesnook.Inbox.API/.env.example
Normal file
2
Notesnook.Inbox.API/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
PORT=5181
|
||||
NOTESNOOK_API_SERVER_URL=http://localhost:5264/
|
||||
3
Notesnook.Inbox.API/.gitignore
vendored
Normal file
3
Notesnook.Inbox.API/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
19
Notesnook.Inbox.API/Dockerfile
Normal file
19
Notesnook.Inbox.API/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM oven/bun:1.2.21-slim
|
||||
|
||||
RUN mkdir -p /home/bun/app && chown -R bun:bun /home/bun/app
|
||||
|
||||
WORKDIR /home/bun/app
|
||||
|
||||
USER bun
|
||||
|
||||
COPY --chown=bun:bun package.json bun.lock .
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY --chown=bun:bun . .
|
||||
|
||||
RUN bun run build
|
||||
|
||||
EXPOSE 5181
|
||||
|
||||
CMD ["bun", "run", "start"]
|
||||
192
Notesnook.Inbox.API/bun.lock
Normal file
192
Notesnook.Inbox.API/bun.lock
Normal file
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "notesnook-inbox-api",
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"libsodium-wrappers-sumo": "^0.7.15",
|
||||
"zod": "^4.1.9",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/libsodium-wrappers-sumo": "^0.7.8",
|
||||
"@types/node": "^24.5.2",
|
||||
"typescript": "^5.9.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="],
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/libsodium-wrappers": ["@types/libsodium-wrappers@0.7.14", "", {}, "sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ=="],
|
||||
|
||||
"@types/libsodium-wrappers-sumo": ["@types/libsodium-wrappers-sumo@0.7.8", "", { "dependencies": { "@types/libsodium-wrappers": "*" } }, "sha512-N2+df4MB/A+W0RAcTw7A5oxKgzD+Vh6Ye7lfjWIi5SdTzVLfHPzxUjhwPqHLO5Ev9fv/+VHl+sUaUuTg4fUPqw=="],
|
||||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="],
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"libsodium-sumo": ["libsodium-sumo@0.7.15", "", {}, "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw=="],
|
||||
|
||||
"libsodium-wrappers-sumo": ["libsodium-wrappers-sumo@0.7.15", "", { "dependencies": { "libsodium-sumo": "^0.7.15" } }, "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@4.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="],
|
||||
|
||||
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||
|
||||
"raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
||||
}
|
||||
}
|
||||
33
Notesnook.Inbox.API/package.json
Normal file
33
Notesnook.Inbox.API/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "notesnook-inbox-api",
|
||||
"version": "1.0.0",
|
||||
"description": "Notesnook Inbox API server",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "bun build src/index.ts --outdir dist --target bun",
|
||||
"start": "bun run dist/index.js",
|
||||
"dev": "bun --watch src/index.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"notesnook",
|
||||
"inbox",
|
||||
"api"
|
||||
],
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Streetwriters (Private) Limited",
|
||||
"email": "support@streetwriters.co",
|
||||
"url": "https://streetwriters.co"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"libsodium-wrappers-sumo": "^0.7.15",
|
||||
"zod": "^4.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/libsodium-wrappers-sumo": "^0.7.8",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^24.5.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
173
Notesnook.Inbox.API/src/index.ts
Normal file
173
Notesnook.Inbox.API/src/index.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import express from "express";
|
||||
import _sodium, { base64_variants } from "libsodium-wrappers-sumo";
|
||||
import { z } from "zod";
|
||||
|
||||
const NOTESNOOK_API_SERVER_URL = process.env.NOTESNOOK_API_SERVER_URL;
|
||||
if (!NOTESNOOK_API_SERVER_URL) {
|
||||
throw new Error("NOTESNOOK_API_SERVER_URL is not defined");
|
||||
}
|
||||
|
||||
let sodium: typeof _sodium;
|
||||
|
||||
const RawInboxItemSchema = z.object({
|
||||
title: z.string().min(1, "Title is required"),
|
||||
pinned: z.boolean().optional(),
|
||||
favorite: z.boolean().optional(),
|
||||
readonly: z.boolean().optional(),
|
||||
archived: z.boolean().optional(),
|
||||
notebookIds: z.array(z.string()).optional(),
|
||||
tagIds: z.array(z.string()).optional(),
|
||||
type: z.enum(["note"]),
|
||||
source: z.string(),
|
||||
version: z.literal(1),
|
||||
content: z
|
||||
.object({
|
||||
type: z.enum(["html"]),
|
||||
data: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
interface EncryptedInboxItem {
|
||||
v: 1;
|
||||
key: Omit<EncryptedInboxItem, "key" | "iv" | "v">;
|
||||
iv: string;
|
||||
alg: string;
|
||||
cipher: string;
|
||||
length: number;
|
||||
}
|
||||
|
||||
function encrypt(rawData: string, publicKey: string): EncryptedInboxItem {
|
||||
try {
|
||||
const password = sodium.crypto_aead_xchacha20poly1305_ietf_keygen();
|
||||
const nonce = sodium.randombytes_buf(
|
||||
sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
|
||||
);
|
||||
const data = sodium.from_string(rawData);
|
||||
const cipher = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||
data,
|
||||
null,
|
||||
null,
|
||||
nonce,
|
||||
password
|
||||
);
|
||||
const inboxPublicKey = sodium.from_base64(
|
||||
publicKey,
|
||||
base64_variants.URLSAFE_NO_PADDING
|
||||
);
|
||||
const encryptedPassword = sodium.crypto_box_seal(password, inboxPublicKey);
|
||||
|
||||
return {
|
||||
v: 1,
|
||||
key: {
|
||||
cipher: sodium.to_base64(
|
||||
encryptedPassword,
|
||||
base64_variants.URLSAFE_NO_PADDING
|
||||
),
|
||||
alg: `xsal-x25519-${base64_variants.URLSAFE_NO_PADDING}`,
|
||||
length: password.length,
|
||||
},
|
||||
iv: sodium.to_base64(nonce, base64_variants.URLSAFE_NO_PADDING),
|
||||
alg: `xcha-argon2i13-${base64_variants.URLSAFE_NO_PADDING}`,
|
||||
cipher: sodium.to_base64(cipher, base64_variants.URLSAFE_NO_PADDING),
|
||||
length: data.length,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`encryption failed: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getInboxPublicEncryptionKey(apiKey: string) {
|
||||
const response = await fetch(
|
||||
`${NOTESNOOK_API_SERVER_URL}inbox/public-encryption-key`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: apiKey,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`failed to fetch inbox public encryption key: ${await response.text()}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as unknown as any;
|
||||
return (data?.key as string) || null;
|
||||
}
|
||||
|
||||
async function postEncryptedInboxItem(
|
||||
apiKey: string,
|
||||
item: EncryptedInboxItem
|
||||
) {
|
||||
const response = await fetch(`${NOTESNOOK_API_SERVER_URL}inbox/items`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: apiKey,
|
||||
},
|
||||
body: JSON.stringify({ ...item }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`failed to post inbox item: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
app.post("/inbox", async (req, res) => {
|
||||
try {
|
||||
const apiKey = req.headers["authorization"];
|
||||
if (!apiKey) {
|
||||
return res.status(401).json({ error: "unauthorized" });
|
||||
}
|
||||
if (!req.body.item) {
|
||||
return res.status(400).json({ error: "item is required" });
|
||||
}
|
||||
|
||||
const validationResult = RawInboxItemSchema.safeParse(req.body.item);
|
||||
if (!validationResult.success) {
|
||||
return res.status(400).json({
|
||||
error: "invalid item",
|
||||
details: validationResult.error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
const inboxPublicKey = await getInboxPublicEncryptionKey(apiKey);
|
||||
if (!inboxPublicKey) {
|
||||
return res.status(403).json({ error: "inbox public key not found" });
|
||||
}
|
||||
console.log("[info] fetched inbox public key:", inboxPublicKey);
|
||||
|
||||
const item = validationResult.data;
|
||||
const encryptedItem = encrypt(JSON.stringify(item), inboxPublicKey);
|
||||
console.log("[info] encrypted item:", encryptedItem);
|
||||
await postEncryptedInboxItem(apiKey, encryptedItem);
|
||||
return res.status(200).json({ message: "inbox item posted" });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log("[error]", error.message);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "internal server error", description: error.message });
|
||||
} else {
|
||||
console.log("[error] unknown error occured:", error);
|
||||
return res.status(500).json({
|
||||
error: "internal server error",
|
||||
description: `unknown error occured: ${error}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await _sodium.ready;
|
||||
sodium = _sodium;
|
||||
|
||||
const PORT = Number(process.env.PORT || "5181");
|
||||
app.listen(PORT, () => {
|
||||
console.log(`📫 notesnook inbox api server running on port ${PORT}`);
|
||||
});
|
||||
})();
|
||||
|
||||
export default app;
|
||||
13
Notesnook.Inbox.API/tsconfig.json
Normal file
13
Notesnook.Inbox.API/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -70,6 +70,7 @@ namespace Streetwriters.Common
|
||||
public static string SSE_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SSE_CERT_KEY_PATH");
|
||||
|
||||
// internal
|
||||
public static string WEBRISK_API_URI => Environment.GetEnvironmentVariable("WEBRISK_API_URI");
|
||||
public static string MONGODB_CONNECTION_STRING => Environment.GetEnvironmentVariable("MONGODB_CONNECTION_STRING");
|
||||
public static string MONGODB_DATABASE_NAME => Environment.GetEnvironmentVariable("MONGODB_DATABASE_NAME");
|
||||
public static int SUBSCRIPTIONS_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_PORT") ?? "80");
|
||||
|
||||
33
Streetwriters.Common/Enums/SubscriptionPlan.cs
Normal file
33
Streetwriters.Common/Enums/SubscriptionPlan.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
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;
|
||||
|
||||
namespace Streetwriters.Common.Enums
|
||||
{
|
||||
public enum SubscriptionPlan
|
||||
{
|
||||
FREE = 0,
|
||||
ESSENTIAL = 1,
|
||||
PRO = 2,
|
||||
BELIEVER = 3,
|
||||
EDUCATION = 4,
|
||||
LEGACY_PRO = 5
|
||||
}
|
||||
}
|
||||
30
Streetwriters.Common/Enums/SubscriptionStatus.cs
Normal file
30
Streetwriters.Common/Enums/SubscriptionStatus.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
namespace Streetwriters.Common.Enums
|
||||
{
|
||||
public enum SubscriptionStatus
|
||||
{
|
||||
ACTIVE,
|
||||
TRIAL,
|
||||
CANCELED,
|
||||
PAUSED,
|
||||
EXPIRED
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
namespace Streetwriters.Common.Enums
|
||||
{
|
||||
public enum SubscriptionType
|
||||
{
|
||||
BASIC = 0,
|
||||
TRIAL = 1,
|
||||
BETA = 2,
|
||||
PREMIUM = 5,
|
||||
PREMIUM_EXPIRED = 6,
|
||||
PREMIUM_CANCELED = 7,
|
||||
PREMIUM_PAUSED = 8
|
||||
}
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
namespace Streetwriters.Common.Enums
|
||||
{
|
||||
public enum SubscriptionType
|
||||
{
|
||||
BASIC = 0,
|
||||
TRIAL = 1,
|
||||
BETA = 2,
|
||||
PREMIUM = 5,
|
||||
PREMIUM_EXPIRED = 6,
|
||||
PREMIUM_CANCELED = 7,
|
||||
PREMIUM_PAUSED = 8,
|
||||
}
|
||||
}
|
||||
@@ -37,19 +37,21 @@ namespace Streetwriters.Common.Extensions
|
||||
request.Content = content;
|
||||
}
|
||||
|
||||
foreach (var header in headers)
|
||||
if (headers != null)
|
||||
{
|
||||
if (header.Key == "Content-Type" || header.Key == "Content-Length")
|
||||
foreach (var header in headers)
|
||||
{
|
||||
if (request.Content != null)
|
||||
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
|
||||
continue;
|
||||
if (header.Key == "Content-Type" || header.Key == "Content-Length")
|
||||
{
|
||||
request.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
|
||||
continue;
|
||||
}
|
||||
request.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
|
||||
}
|
||||
request.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
|
||||
}
|
||||
|
||||
var response = await httpClient.SendAsync(request);
|
||||
if (response.Content.Headers.ContentLength > 0)
|
||||
if (response.Content.Headers.ContentLength > 0 && response.Content.Headers.ContentType.ToString().Contains("application/json"))
|
||||
{
|
||||
var res = await response.Content.ReadFromJsonAsync<T>();
|
||||
res.Success = response.IsSuccessStatusCode;
|
||||
@@ -58,7 +60,7 @@ namespace Streetwriters.Common.Extensions
|
||||
}
|
||||
else
|
||||
{
|
||||
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode };
|
||||
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode, Content = response.Content };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using WampSharp.AspNetCore.WebSockets.Server;
|
||||
@@ -38,5 +40,27 @@ namespace Streetwriters.Common.Extensions
|
||||
{
|
||||
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(async (message) => await handler.Process(message));
|
||||
}
|
||||
|
||||
public static IDisposable SubscribeWithSemaphore<T>(this IWampHostedRealm realm, string topicName, IMessageHandler<T> handler)
|
||||
{
|
||||
var semaphore = new SemaphoreSlim(1, 1);
|
||||
var subscriber = realm.Services.GetSubject<T>(topicName).Subscribe<T>(async (message) =>
|
||||
{
|
||||
await semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
await handler.Process(message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
});
|
||||
return Disposable.Create(() =>
|
||||
{
|
||||
subscriber.Dispose();
|
||||
semaphore.Dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,55 @@
|
||||
/*
|
||||
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.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using Streetwriters.Common.Messages;
|
||||
using WampSharp.V2;
|
||||
using WampSharp.V2.Client;
|
||||
|
||||
namespace Streetwriters.Common.Helpers
|
||||
{
|
||||
public class WampHelper
|
||||
{
|
||||
public static async Task<IWampRealmProxy> OpenWampChannelAsync(string server, string realmName)
|
||||
{
|
||||
DefaultWampChannelFactory channelFactory = new();
|
||||
|
||||
IWampChannel channel = channelFactory.CreateJsonChannel(server, realmName);
|
||||
|
||||
await channel.Open();
|
||||
|
||||
return channel.RealmProxy;
|
||||
}
|
||||
|
||||
public static void PublishMessage<T>(IWampRealmProxy realm, string topicName, T message)
|
||||
{
|
||||
var subject = realm.Services.GetSubject<T>(topicName);
|
||||
subject.OnNext(message);
|
||||
}
|
||||
}
|
||||
/*
|
||||
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.Collections.Generic;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using Streetwriters.Common.Messages;
|
||||
using WampSharp.V2;
|
||||
using WampSharp.V2.Client;
|
||||
|
||||
namespace Streetwriters.Common.Helpers
|
||||
{
|
||||
public class WampHelper
|
||||
{
|
||||
public static async Task<IWampRealmProxy> OpenWampChannelAsync(string server, string realmName)
|
||||
{
|
||||
DefaultWampChannelFactory channelFactory = new();
|
||||
|
||||
IWampChannel channel = channelFactory.CreateJsonChannel(server, realmName);
|
||||
|
||||
await channel.Open();
|
||||
|
||||
return channel.RealmProxy;
|
||||
}
|
||||
|
||||
public static void PublishMessage<T>(IWampRealmProxy realm, string topicName, T message)
|
||||
{
|
||||
var subject = realm.Services.GetSubject<T>(topicName);
|
||||
subject.OnNext(message);
|
||||
}
|
||||
|
||||
public static void PublishMessages<T>(IWampRealmProxy realm, string topicName, IEnumerable<T> messages)
|
||||
{
|
||||
var subject = realm.Services.GetSubject<T>(topicName);
|
||||
foreach (var message in messages)
|
||||
subject.OnNext(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,14 @@ 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.Net.Http;
|
||||
|
||||
namespace Streetwriters.Common.Interfaces
|
||||
{
|
||||
public interface IResponse
|
||||
{
|
||||
bool Success { get; set; }
|
||||
int StatusCode { get; set; }
|
||||
HttpContent Content { get; set; }
|
||||
}
|
||||
}
|
||||
13
Streetwriters.Common/Interfaces/IURLAnalyzer.cs
Normal file
13
Streetwriters.Common/Interfaces/IURLAnalyzer.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MimeKit;
|
||||
using MimeKit.Cryptography;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Streetwriters.Common.Interfaces
|
||||
{
|
||||
public interface IURLAnalyzer
|
||||
{
|
||||
Task<bool> IsURLSafeAsync(string uri);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Streetwriters.Common.Interfaces
|
||||
public interface IUserAccountService
|
||||
{
|
||||
[WampProcedure("co.streetwriters.identity.users.get_user")]
|
||||
Task<UserModel> GetUserAsync(string clientId, string userId);
|
||||
Task<UserModel?> GetUserAsync(string clientId, string userId);
|
||||
[WampProcedure("co.streetwriters.identity.users.delete_user")]
|
||||
Task DeleteUserAsync(string clientId, string userId, string password);
|
||||
// [WampProcedure("co.streetwriters.identity.users.create_user")]
|
||||
|
||||
@@ -1,63 +1,66 @@
|
||||
/*
|
||||
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.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
|
||||
namespace Streetwriters.Common.Messages
|
||||
{
|
||||
public class CreateSubscriptionMessage
|
||||
{
|
||||
[JsonPropertyName("userId")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("provider")]
|
||||
public SubscriptionProvider Provider { get; set; }
|
||||
|
||||
[JsonPropertyName("appId")]
|
||||
public ApplicationType AppId { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public SubscriptionType Type { get; set; }
|
||||
|
||||
[JsonPropertyName("start")]
|
||||
public long StartTime { get; set; }
|
||||
|
||||
[JsonPropertyName("expiry")]
|
||||
public long ExpiryTime { get; set; }
|
||||
|
||||
[JsonPropertyName("orderId")]
|
||||
public string OrderId { get; set; }
|
||||
|
||||
[JsonPropertyName("updateURL")]
|
||||
public string UpdateURL { get; set; }
|
||||
|
||||
[JsonPropertyName("cancelURL")]
|
||||
public string CancelURL { get; set; }
|
||||
|
||||
[JsonPropertyName("subscriptionId")]
|
||||
public string SubscriptionId { get; set; }
|
||||
|
||||
[JsonPropertyName("productId")]
|
||||
public string ProductId { get; set; }
|
||||
public bool Extend { get; set; }
|
||||
}
|
||||
/*
|
||||
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.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
|
||||
namespace Streetwriters.Common.Messages
|
||||
{
|
||||
public class CreateSubscriptionMessage
|
||||
{
|
||||
[JsonPropertyName("userId")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("provider")]
|
||||
public SubscriptionProvider Provider { get; set; }
|
||||
|
||||
[JsonPropertyName("appId")]
|
||||
public ApplicationType AppId { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public SubscriptionType Type { get; set; }
|
||||
|
||||
[JsonPropertyName("start")]
|
||||
public long StartTime { get; set; }
|
||||
|
||||
[JsonPropertyName("expiry")]
|
||||
public long ExpiryTime { get; set; }
|
||||
|
||||
[JsonPropertyName("orderId")]
|
||||
public string OrderId { get; set; }
|
||||
|
||||
[JsonPropertyName("updateURL")]
|
||||
public string UpdateURL { get; set; }
|
||||
|
||||
[JsonPropertyName("cancelURL")]
|
||||
public string CancelURL { get; set; }
|
||||
|
||||
[JsonPropertyName("subscriptionId")]
|
||||
public string SubscriptionId { get; set; }
|
||||
|
||||
[JsonPropertyName("productId")]
|
||||
public string ProductId { get; set; }
|
||||
|
||||
[JsonPropertyName("extend")]
|
||||
public bool Extend { get; set; }
|
||||
}
|
||||
}
|
||||
69
Streetwriters.Common/Messages/CreateSubscriptionMessageV2.cs
Normal file
69
Streetwriters.Common/Messages/CreateSubscriptionMessageV2.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
|
||||
namespace Streetwriters.Common.Messages
|
||||
{
|
||||
public class CreateSubscriptionMessageV2
|
||||
{
|
||||
[JsonPropertyName("userId")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("provider")]
|
||||
public SubscriptionProvider Provider { get; set; }
|
||||
|
||||
[JsonPropertyName("appId")]
|
||||
public ApplicationType AppId { get; set; }
|
||||
|
||||
[JsonPropertyName("plan")]
|
||||
public SubscriptionPlan Plan { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public SubscriptionStatus Status { get; set; }
|
||||
|
||||
[JsonPropertyName("start")]
|
||||
public long StartTime { get; set; }
|
||||
|
||||
[JsonPropertyName("expiry")]
|
||||
public long ExpiryTime { get; set; }
|
||||
|
||||
[JsonPropertyName("orderId")]
|
||||
public string OrderId { get; set; }
|
||||
|
||||
[JsonPropertyName("subscriptionId")]
|
||||
public string SubscriptionId { get; set; }
|
||||
|
||||
[JsonPropertyName("productId")]
|
||||
public string ProductId { get; set; }
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public long Timestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("trialExpiry")]
|
||||
public long TrialExpiryTime { get; set; }
|
||||
|
||||
[JsonPropertyName("googlePurchaseToken")]
|
||||
public string? GooglePurchaseToken { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,6 @@ namespace Streetwriters.Common.Messages
|
||||
public Message Message { get; set; }
|
||||
|
||||
[JsonPropertyName("originTokenId")]
|
||||
public string OriginTokenId { get; set; }
|
||||
public string? OriginTokenId { get; set; }
|
||||
}
|
||||
}
|
||||
21
Streetwriters.Common/Models/GetCustomerResponse.cs
Normal file
21
Streetwriters.Common/Models/GetCustomerResponse.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Globalization;
|
||||
|
||||
public partial class GetCustomerResponse : PaddleResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public PaddleCustomer Customer { get; set; }
|
||||
}
|
||||
|
||||
public class PaddleCustomer
|
||||
{
|
||||
[JsonPropertyName("email")]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,214 @@
|
||||
/*
|
||||
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.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
public class SubscriptionResponse : Response
|
||||
{
|
||||
[JsonPropertyName("subscription")]
|
||||
public ISubscription Subscription { get; set; }
|
||||
}
|
||||
}
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Globalization;
|
||||
|
||||
public partial class GetSubscriptionResponse : PaddleResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public Data Data { get; set; }
|
||||
}
|
||||
|
||||
public partial class Data
|
||||
{
|
||||
// [JsonPropertyName("id")]
|
||||
// public string Id { get; set; }
|
||||
|
||||
// [JsonPropertyName("status")]
|
||||
// public string Status { get; set; }
|
||||
|
||||
[JsonPropertyName("customer_id")]
|
||||
public string CustomerId { get; set; }
|
||||
|
||||
// [JsonPropertyName("address_id")]
|
||||
// public string AddressId { get; set; }
|
||||
|
||||
// [JsonPropertyName("business_id")]
|
||||
// public object BusinessId { get; set; }
|
||||
|
||||
// [JsonPropertyName("currency_code")]
|
||||
// public string CurrencyCode { get; set; }
|
||||
|
||||
// [JsonPropertyName("created_at")]
|
||||
// public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("updated_at")]
|
||||
// public DateTimeOffset UpdatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("started_at")]
|
||||
// public DateTimeOffset StartedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("first_billed_at")]
|
||||
public DateTimeOffset? FirstBilledAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("next_billed_at")]
|
||||
// public DateTimeOffset NextBilledAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("paused_at")]
|
||||
// public object PausedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("canceled_at")]
|
||||
// public object CanceledAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("collection_mode")]
|
||||
// public string CollectionMode { get; set; }
|
||||
|
||||
// [JsonPropertyName("billing_details")]
|
||||
// public object BillingDetails { get; set; }
|
||||
|
||||
// [JsonPropertyName("current_billing_period")]
|
||||
// public CurrentBillingPeriod CurrentBillingPeriod { get; set; }
|
||||
|
||||
[JsonPropertyName("billing_cycle")]
|
||||
public BillingCycle BillingCycle { get; set; }
|
||||
|
||||
// [JsonPropertyName("scheduled_change")]
|
||||
// public object ScheduledChange { get; set; }
|
||||
|
||||
// [JsonPropertyName("items")]
|
||||
// public Item[] Items { get; set; }
|
||||
|
||||
// [JsonPropertyName("custom_data")]
|
||||
// public object CustomData { get; set; }
|
||||
|
||||
[JsonPropertyName("management_urls")]
|
||||
public ManagementUrls ManagementUrls { get; set; }
|
||||
|
||||
// [JsonPropertyName("discount")]
|
||||
// public object Discount { get; set; }
|
||||
|
||||
// [JsonPropertyName("import_meta")]
|
||||
// public object ImportMeta { get; set; }
|
||||
}
|
||||
|
||||
public partial class BillingCycle
|
||||
{
|
||||
[JsonPropertyName("frequency")]
|
||||
public long Frequency { get; set; }
|
||||
|
||||
[JsonPropertyName("interval")]
|
||||
public string Interval { get; set; }
|
||||
}
|
||||
|
||||
// public partial class CurrentBillingPeriod
|
||||
// {
|
||||
// [JsonPropertyName("starts_at")]
|
||||
// public DateTimeOffset StartsAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("ends_at")]
|
||||
// public DateTimeOffset EndsAt { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class Item
|
||||
// {
|
||||
// [JsonPropertyName("status")]
|
||||
// public string Status { get; set; }
|
||||
|
||||
// [JsonPropertyName("quantity")]
|
||||
// public long Quantity { get; set; }
|
||||
|
||||
// [JsonPropertyName("recurring")]
|
||||
// public bool Recurring { get; set; }
|
||||
|
||||
// [JsonPropertyName("created_at")]
|
||||
// public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("updated_at")]
|
||||
// public DateTimeOffset UpdatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("previously_billed_at")]
|
||||
// public DateTimeOffset PreviouslyBilledAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("next_billed_at")]
|
||||
// public DateTimeOffset NextBilledAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("trial_dates")]
|
||||
// public object TrialDates { get; set; }
|
||||
|
||||
// [JsonPropertyName("price")]
|
||||
// public Price Price { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class Price
|
||||
// {
|
||||
// [JsonPropertyName("id")]
|
||||
// public string Id { get; set; }
|
||||
|
||||
// [JsonPropertyName("product_id")]
|
||||
// public string ProductId { get; set; }
|
||||
|
||||
// [JsonPropertyName("type")]
|
||||
// public string Type { get; set; }
|
||||
|
||||
// [JsonPropertyName("description")]
|
||||
// public string Description { get; set; }
|
||||
|
||||
// [JsonPropertyName("name")]
|
||||
// public string Name { get; set; }
|
||||
|
||||
// [JsonPropertyName("tax_mode")]
|
||||
// public string TaxMode { get; set; }
|
||||
|
||||
// [JsonPropertyName("billing_cycle")]
|
||||
// public BillingCycle BillingCycle { get; set; }
|
||||
|
||||
// [JsonPropertyName("trial_period")]
|
||||
// public object TrialPeriod { get; set; }
|
||||
|
||||
// [JsonPropertyName("unit_price")]
|
||||
// public UnitPrice UnitPrice { get; set; }
|
||||
|
||||
// [JsonPropertyName("unit_price_overrides")]
|
||||
// public object[] UnitPriceOverrides { get; set; }
|
||||
|
||||
// [JsonPropertyName("custom_data")]
|
||||
// public object CustomData { get; set; }
|
||||
|
||||
// [JsonPropertyName("status")]
|
||||
// public string Status { get; set; }
|
||||
|
||||
// [JsonPropertyName("quantity")]
|
||||
// public Quantity Quantity { get; set; }
|
||||
|
||||
// [JsonPropertyName("import_meta")]
|
||||
// public object ImportMeta { get; set; }
|
||||
|
||||
// [JsonPropertyName("created_at")]
|
||||
// public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("updated_at")]
|
||||
// public DateTimeOffset UpdatedAt { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class Quantity
|
||||
// {
|
||||
// [JsonPropertyName("minimum")]
|
||||
// public long Minimum { get; set; }
|
||||
|
||||
// [JsonPropertyName("maximum")]
|
||||
// public long Maximum { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class UnitPrice
|
||||
// {
|
||||
// [JsonPropertyName("amount")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Amount { get; set; }
|
||||
|
||||
// [JsonPropertyName("currency_code")]
|
||||
// public string CurrencyCode { get; set; }
|
||||
// }
|
||||
|
||||
public partial class ManagementUrls
|
||||
{
|
||||
[JsonPropertyName("update_payment_method")]
|
||||
public Uri UpdatePaymentMethod { get; set; }
|
||||
|
||||
[JsonPropertyName("cancel")]
|
||||
public Uri Cancel { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
21
Streetwriters.Common/Models/GetTransactionInvoiceResponse.cs
Normal file
21
Streetwriters.Common/Models/GetTransactionInvoiceResponse.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Globalization;
|
||||
|
||||
public class GetTransactionInvoiceResponse : PaddleResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public Invoice Invoice { get; set; }
|
||||
}
|
||||
|
||||
public partial class Invoice
|
||||
{
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
||||
15
Streetwriters.Common/Models/GetTransactionResponse.cs
Normal file
15
Streetwriters.Common/Models/GetTransactionResponse.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Globalization;
|
||||
|
||||
public partial class GetTransactionResponse : PaddleResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public TransactionV2 Transaction { get; set; }
|
||||
}
|
||||
}
|
||||
41
Streetwriters.Common/Models/ListPaymentsResponse.cs
Normal file
41
Streetwriters.Common/Models/ListPaymentsResponse.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
public partial class ListPaymentsResponse
|
||||
{
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("response")]
|
||||
public Payment[] Payments { get; set; }
|
||||
}
|
||||
|
||||
public partial class Payment
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[JsonPropertyName("subscription_id")]
|
||||
public long SubscriptionId { get; set; }
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
public double Amount { get; set; }
|
||||
|
||||
[JsonPropertyName("currency")]
|
||||
public string Currency { get; set; }
|
||||
|
||||
[JsonPropertyName("payout_date")]
|
||||
public string PayoutDate { get; set; }
|
||||
|
||||
[JsonPropertyName("is_paid")]
|
||||
public short IsPaid { get; set; }
|
||||
|
||||
[JsonPropertyName("is_one_off_charge")]
|
||||
public bool IsOneOffCharge { get; set; }
|
||||
|
||||
[JsonPropertyName("receipt_url")]
|
||||
public string ReceiptUrl { get; set; }
|
||||
}
|
||||
}
|
||||
77
Streetwriters.Common/Models/ListTransactionsResponse.cs
Normal file
77
Streetwriters.Common/Models/ListTransactionsResponse.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public partial class ListTransactionsResponse
|
||||
{
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("response")]
|
||||
public Transaction[] Transactions { get; set; }
|
||||
}
|
||||
|
||||
public partial class Transaction
|
||||
{
|
||||
[JsonPropertyName("order_id")]
|
||||
public string OrderId { get; set; }
|
||||
|
||||
[JsonPropertyName("checkout_id")]
|
||||
public string CheckoutId { get; set; }
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
public string Amount { get; set; }
|
||||
|
||||
[JsonPropertyName("currency")]
|
||||
public string Currency { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("passthrough")]
|
||||
public object Passthrough { get; set; }
|
||||
|
||||
[JsonPropertyName("product_id")]
|
||||
public long ProductId { get; set; }
|
||||
|
||||
[JsonPropertyName("is_subscription")]
|
||||
public bool IsSubscription { get; set; }
|
||||
|
||||
[JsonPropertyName("is_one_off")]
|
||||
public bool IsOneOff { get; set; }
|
||||
|
||||
[JsonPropertyName("subscription")]
|
||||
public PaddleSubscription Subscription { get; set; }
|
||||
|
||||
[JsonPropertyName("user")]
|
||||
public PaddleTransactionUser User { get; set; }
|
||||
|
||||
[JsonPropertyName("receipt_url")]
|
||||
public string ReceiptUrl { get; set; }
|
||||
}
|
||||
|
||||
public partial class PaddleSubscription
|
||||
{
|
||||
[JsonPropertyName("subscription_id")]
|
||||
public long SubscriptionId { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; }
|
||||
}
|
||||
|
||||
public partial class PaddleTransactionUser
|
||||
{
|
||||
[JsonPropertyName("user_id")]
|
||||
public long UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonPropertyName("marketing_consent")]
|
||||
public bool MarketingConsent { get; set; }
|
||||
}
|
||||
}
|
||||
511
Streetwriters.Common/Models/ListTransactionsResponseV2.cs
Normal file
511
Streetwriters.Common/Models/ListTransactionsResponseV2.cs
Normal file
@@ -0,0 +1,511 @@
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Globalization;
|
||||
|
||||
public partial class ListTransactionsResponseV2 : PaddleResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public TransactionV2[] Transactions { get; set; }
|
||||
}
|
||||
|
||||
public partial class TransactionV2
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonPropertyName("customer_id")]
|
||||
public string CustomerId { get; set; }
|
||||
|
||||
// [JsonPropertyName("address_id")]
|
||||
// public string AddressId { get; set; }
|
||||
|
||||
// [JsonPropertyName("business_id")]
|
||||
// public object BusinessId { get; set; }
|
||||
|
||||
[JsonPropertyName("custom_data")]
|
||||
public Dictionary<string, string> CustomData { get; set; }
|
||||
|
||||
[JsonPropertyName("origin")]
|
||||
public string Origin { get; set; }
|
||||
|
||||
// [JsonPropertyName("collection_mode")]
|
||||
// public string CollectionMode { get; set; }
|
||||
|
||||
// [JsonPropertyName("subscription_id")]
|
||||
// public string SubscriptionId { get; set; }
|
||||
|
||||
// [JsonPropertyName("invoice_id")]
|
||||
// public string InvoiceId { get; set; }
|
||||
|
||||
// [JsonPropertyName("invoice_number")]
|
||||
// public string InvoiceNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("billing_details")]
|
||||
public BillingDetails BillingDetails { get; set; }
|
||||
|
||||
[JsonPropertyName("billing_period")]
|
||||
public BillingPeriod BillingPeriod { get; set; }
|
||||
|
||||
// [JsonPropertyName("currency_code")]
|
||||
// public string CurrencyCode { get; set; }
|
||||
|
||||
// [JsonPropertyName("discount_id")]
|
||||
// public string DiscountId { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("updated_at")]
|
||||
// public DateTimeOffset UpdatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("billed_at")]
|
||||
public DateTimeOffset? BilledAt { get; set; }
|
||||
|
||||
[JsonPropertyName("items")]
|
||||
public Item[] Items { get; set; }
|
||||
|
||||
[JsonPropertyName("details")]
|
||||
public Details Details { get; set; }
|
||||
|
||||
// [JsonPropertyName("payments")]
|
||||
// public Payment[] Payments { get; set; }
|
||||
|
||||
// [JsonPropertyName("checkout")]
|
||||
// public Checkout Checkout { get; set; }
|
||||
}
|
||||
|
||||
public partial class BillingDetails
|
||||
{
|
||||
// [JsonPropertyName("enable_checkout")]
|
||||
// public bool EnableCheckout { get; set; }
|
||||
|
||||
[JsonPropertyName("payment_terms")]
|
||||
public PaymentTerms PaymentTerms { get; set; }
|
||||
|
||||
// [JsonPropertyName("purchase_order_number")]
|
||||
// public string PurchaseOrderNumber { get; set; }
|
||||
|
||||
// [JsonPropertyName("additional_information")]
|
||||
// public object AdditionalInformation { get; set; }
|
||||
}
|
||||
|
||||
public partial class PaymentTerms
|
||||
{
|
||||
[JsonPropertyName("interval")]
|
||||
public string Interval { get; set; }
|
||||
|
||||
[JsonPropertyName("frequency")]
|
||||
public long Frequency { get; set; }
|
||||
}
|
||||
|
||||
public partial class BillingPeriod
|
||||
{
|
||||
[JsonPropertyName("starts_at")]
|
||||
public DateTimeOffset StartsAt { get; set; }
|
||||
|
||||
[JsonPropertyName("ends_at")]
|
||||
public DateTimeOffset EndsAt { get; set; }
|
||||
}
|
||||
|
||||
// public partial class Checkout
|
||||
// {
|
||||
// [JsonPropertyName("url")]
|
||||
// public Uri Url { get; set; }
|
||||
// }
|
||||
|
||||
public partial class Details
|
||||
{
|
||||
// [JsonPropertyName("tax_rates_used")]
|
||||
// public TaxRatesUsed[] TaxRatesUsed { get; set; }
|
||||
|
||||
[JsonPropertyName("totals")]
|
||||
public Totals Totals { get; set; }
|
||||
|
||||
// [JsonPropertyName("adjusted_totals")]
|
||||
// public AdjustedTotals AdjustedTotals { get; set; }
|
||||
|
||||
// [JsonPropertyName("payout_totals")]
|
||||
// public Dictionary<string, string> PayoutTotals { get; set; }
|
||||
|
||||
// [JsonPropertyName("adjusted_payout_totals")]
|
||||
// public AdjustedTotals AdjustedPayoutTotals { get; set; }
|
||||
|
||||
[JsonPropertyName("line_items")]
|
||||
public LineItem[] LineItems { get; set; }
|
||||
}
|
||||
|
||||
public partial class Totals
|
||||
{
|
||||
[JsonPropertyName("subtotal")]
|
||||
public long Subtotal { get; set; }
|
||||
|
||||
[JsonPropertyName("tax")]
|
||||
public long Tax { get; set; }
|
||||
|
||||
[JsonPropertyName("discount")]
|
||||
public long Discount { get; set; }
|
||||
|
||||
[JsonPropertyName("total")]
|
||||
public long Total { get; set; }
|
||||
|
||||
[JsonPropertyName("grand_total")]
|
||||
public long GrandTotal { get; set; }
|
||||
|
||||
// [JsonPropertyName("fee")]
|
||||
// public object Fee { get; set; }
|
||||
|
||||
// [JsonPropertyName("credit")]
|
||||
// public long Credit { get; set; }
|
||||
|
||||
// [JsonPropertyName("credit_to_balance")]
|
||||
// public long CreditToBalance { get; set; }
|
||||
|
||||
[JsonPropertyName("balance")]
|
||||
public long Balance { get; set; }
|
||||
|
||||
// [JsonPropertyName("earnings")]
|
||||
// public object Earnings { get; set; }
|
||||
|
||||
[JsonPropertyName("currency_code")]
|
||||
public string CurrencyCode { get; set; }
|
||||
}
|
||||
// public partial class AdjustedTotals
|
||||
// {
|
||||
// [JsonPropertyName("subtotal")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Subtotal { get; set; }
|
||||
|
||||
// [JsonPropertyName("tax")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Tax { get; set; }
|
||||
|
||||
// [JsonPropertyName("total")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Total { get; set; }
|
||||
|
||||
// [JsonPropertyName("fee")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Fee { get; set; }
|
||||
|
||||
// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
// [JsonPropertyName("chargeback_fee")]
|
||||
// public ChargebackFee ChargebackFee { get; set; }
|
||||
|
||||
// [JsonPropertyName("earnings")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Earnings { get; set; }
|
||||
|
||||
// [JsonPropertyName("currency_code")]
|
||||
// public string CurrencyCode { get; set; }
|
||||
|
||||
// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
// [JsonPropertyName("grand_total")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long? GrandTotal { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class ChargebackFee
|
||||
// {
|
||||
// [JsonPropertyName("amount")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Amount { get; set; }
|
||||
|
||||
// [JsonPropertyName("original")]
|
||||
// public object Original { get; set; }
|
||||
// }
|
||||
|
||||
public partial class LineItem
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("price_id")]
|
||||
public string PriceId { get; set; }
|
||||
|
||||
// [JsonPropertyName("quantity")]
|
||||
// public long Quantity { get; set; }
|
||||
|
||||
// [JsonPropertyName("totals")]
|
||||
// public Totals Totals { get; set; }
|
||||
|
||||
// [JsonPropertyName("product")]
|
||||
// public Product Product { get; set; }
|
||||
|
||||
// [JsonPropertyName("tax_rate")]
|
||||
// public string TaxRate { get; set; }
|
||||
|
||||
// [JsonPropertyName("unit_totals")]
|
||||
// public Totals UnitTotals { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyName("proration")]
|
||||
public Proration Proration { get; set; }
|
||||
}
|
||||
|
||||
// public partial class Product
|
||||
// {
|
||||
// [JsonPropertyName("id")]
|
||||
// public string Id { get; set; }
|
||||
|
||||
// [JsonPropertyName("name")]
|
||||
// public string Name { get; set; }
|
||||
|
||||
// [JsonPropertyName("description")]
|
||||
// public string Description { get; set; }
|
||||
|
||||
// [JsonPropertyName("type")]
|
||||
// public TypeEnum Type { get; set; }
|
||||
|
||||
// [JsonPropertyName("tax_category")]
|
||||
// public TypeEnum TaxCategory { get; set; }
|
||||
|
||||
// [JsonPropertyName("image_url")]
|
||||
// public Uri ImageUrl { get; set; }
|
||||
|
||||
// [JsonPropertyName("custom_data")]
|
||||
// public CustomData CustomData { get; set; }
|
||||
|
||||
// [JsonPropertyName("status")]
|
||||
// public Status Status { get; set; }
|
||||
|
||||
// [JsonPropertyName("created_at")]
|
||||
// public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("updated_at")]
|
||||
// public DateTimeOffset UpdatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("import_meta")]
|
||||
// public object ImportMeta { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class CustomData
|
||||
// {
|
||||
// [JsonPropertyName("features")]
|
||||
// public Features Features { get; set; }
|
||||
|
||||
// [JsonPropertyName("suggested_addons")]
|
||||
// public string[] SuggestedAddons { get; set; }
|
||||
|
||||
// [JsonPropertyName("upgrade_description")]
|
||||
// public string UpgradeDescription { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class Features
|
||||
// {
|
||||
// [JsonPropertyName("aircraft_performance")]
|
||||
// public bool AircraftPerformance { get; set; }
|
||||
|
||||
// [JsonPropertyName("compliance_monitoring")]
|
||||
// public bool ComplianceMonitoring { get; set; }
|
||||
|
||||
// [JsonPropertyName("flight_log_management")]
|
||||
// public bool FlightLogManagement { get; set; }
|
||||
|
||||
// [JsonPropertyName("payment_by_invoice")]
|
||||
// public bool PaymentByInvoice { get; set; }
|
||||
|
||||
// [JsonPropertyName("route_planning")]
|
||||
// public bool RoutePlanning { get; set; }
|
||||
|
||||
// [JsonPropertyName("sso")]
|
||||
// public bool Sso { get; set; }
|
||||
// }
|
||||
|
||||
public partial class Proration
|
||||
{
|
||||
[JsonPropertyName("billing_period")]
|
||||
public BillingPeriod BillingPeriod { get; set; }
|
||||
}
|
||||
|
||||
// public partial class Totals
|
||||
// {
|
||||
// [JsonPropertyName("subtotal")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Subtotal { get; set; }
|
||||
|
||||
// [JsonPropertyName("discount")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Discount { get; set; }
|
||||
|
||||
// [JsonPropertyName("tax")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Tax { get; set; }
|
||||
|
||||
// [JsonPropertyName("total")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Total { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class TaxRatesUsed
|
||||
// {
|
||||
// [JsonPropertyName("tax_rate")]
|
||||
// public string TaxRate { get; set; }
|
||||
|
||||
// [JsonPropertyName("totals")]
|
||||
// public Totals Totals { get; set; }
|
||||
// }
|
||||
|
||||
public partial class Item
|
||||
{
|
||||
[JsonPropertyName("price")]
|
||||
public Price Price { get; set; }
|
||||
|
||||
[JsonPropertyName("quantity")]
|
||||
public long Quantity { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyName("proration")]
|
||||
public Proration Proration { get; set; }
|
||||
}
|
||||
|
||||
public partial class Price
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
// [JsonPropertyName("description")]
|
||||
// public string Description { get; set; }
|
||||
|
||||
// [JsonPropertyName("type")]
|
||||
// public TypeEnum Type { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
// [JsonPropertyName("product_id")]
|
||||
// public string ProductId { get; set; }
|
||||
|
||||
// [JsonPropertyName("billing_cycle")]
|
||||
// public PaymentTerms BillingCycle { get; set; }
|
||||
|
||||
// [JsonPropertyName("trial_period")]
|
||||
// public object TrialPeriod { get; set; }
|
||||
|
||||
// [JsonPropertyName("tax_mode")]
|
||||
// public TaxMode TaxMode { get; set; }
|
||||
|
||||
// [JsonPropertyName("unit_price")]
|
||||
// public UnitPrice UnitPrice { get; set; }
|
||||
|
||||
// [JsonPropertyName("unit_price_overrides")]
|
||||
// public object[] UnitPriceOverrides { get; set; }
|
||||
|
||||
// [JsonPropertyName("custom_data")]
|
||||
// public object CustomData { get; set; }
|
||||
|
||||
// [JsonPropertyName("quantity")]
|
||||
// public Quantity Quantity { get; set; }
|
||||
|
||||
// [JsonPropertyName("status")]
|
||||
// public Status Status { get; set; }
|
||||
|
||||
// [JsonPropertyName("created_at")]
|
||||
// public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("updated_at")]
|
||||
// public DateTimeOffset UpdatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("import_meta")]
|
||||
// public object ImportMeta { get; set; }
|
||||
}
|
||||
|
||||
// public partial class Quantity
|
||||
// {
|
||||
// [JsonPropertyName("minimum")]
|
||||
// public long Minimum { get; set; }
|
||||
|
||||
// [JsonPropertyName("maximum")]
|
||||
// public long Maximum { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class UnitPrice
|
||||
// {
|
||||
// [JsonPropertyName("amount")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Amount { get; set; }
|
||||
|
||||
// [JsonPropertyName("currency_code")]
|
||||
// public CurrencyCode CurrencyCode { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class Payment
|
||||
// {
|
||||
// [JsonPropertyName("payment_attempt_id")]
|
||||
// public Guid PaymentAttemptId { get; set; }
|
||||
|
||||
// [JsonPropertyName("stored_payment_method_id")]
|
||||
// public Guid StoredPaymentMethodId { get; set; }
|
||||
|
||||
// [JsonPropertyName("payment_method_id")]
|
||||
// public string PaymentMethodId { get; set; }
|
||||
|
||||
// [JsonPropertyName("amount")]
|
||||
// [JsonConverter(typeof(ParseStringConverter))]
|
||||
// public long Amount { get; set; }
|
||||
|
||||
// [JsonPropertyName("status")]
|
||||
// public string Status { get; set; }
|
||||
|
||||
// [JsonPropertyName("error_code")]
|
||||
// public string ErrorCode { get; set; }
|
||||
|
||||
// [JsonPropertyName("method_details")]
|
||||
// public MethodDetails MethodDetails { get; set; }
|
||||
|
||||
// [JsonPropertyName("created_at")]
|
||||
// public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
// [JsonPropertyName("captured_at")]
|
||||
// public DateTimeOffset? CapturedAt { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class MethodDetails
|
||||
// {
|
||||
// [JsonPropertyName("type")]
|
||||
// public string Type { get; set; }
|
||||
|
||||
// [JsonPropertyName("card")]
|
||||
// public Card Card { get; set; }
|
||||
// }
|
||||
|
||||
// public partial class Card
|
||||
// {
|
||||
// [JsonPropertyName("type")]
|
||||
// public string Type { get; set; }
|
||||
|
||||
// [JsonPropertyName("last4")]
|
||||
// public string Last4 { get; set; }
|
||||
|
||||
// [JsonPropertyName("expiry_month")]
|
||||
// public long ExpiryMonth { get; set; }
|
||||
|
||||
// [JsonPropertyName("expiry_year")]
|
||||
// public long ExpiryYear { get; set; }
|
||||
|
||||
// [JsonPropertyName("cardholder_name")]
|
||||
// public string CardholderName { get; set; }
|
||||
// }
|
||||
|
||||
public partial class Pagination
|
||||
{
|
||||
[JsonPropertyName("per_page")]
|
||||
public long PerPage { get; set; }
|
||||
|
||||
[JsonPropertyName("next")]
|
||||
public Uri Next { get; set; }
|
||||
|
||||
[JsonPropertyName("has_more")]
|
||||
public bool HasMore { get; set; }
|
||||
|
||||
[JsonPropertyName("estimated_total")]
|
||||
public long EstimatedTotal { get; set; }
|
||||
}
|
||||
}
|
||||
47
Streetwriters.Common/Models/ListUsersResponse.cs
Normal file
47
Streetwriters.Common/Models/ListUsersResponse.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
public partial class ListUsersResponse
|
||||
{
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("response")]
|
||||
public PaddleUser[] Users { get; set; }
|
||||
}
|
||||
|
||||
public class PaddleUser
|
||||
{
|
||||
[JsonPropertyName("subscription_id")]
|
||||
public long SubscriptionId { get; set; }
|
||||
|
||||
[JsonPropertyName("plan_id")]
|
||||
public long PlanId { get; set; }
|
||||
|
||||
[JsonPropertyName("user_id")]
|
||||
public long UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("user_email")]
|
||||
public string UserEmail { get; set; }
|
||||
|
||||
[JsonPropertyName("marketing_consent")]
|
||||
public bool MarketingConsent { get; set; }
|
||||
|
||||
[JsonPropertyName("update_url")]
|
||||
public string UpdateUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("cancel_url")]
|
||||
public string CancelUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("state")]
|
||||
public string State { get; set; }
|
||||
|
||||
[JsonPropertyName("signup_date")]
|
||||
public string SignupDate { get; set; }
|
||||
|
||||
[JsonPropertyName("quantity")]
|
||||
public long Quantity { get; set; }
|
||||
}
|
||||
}
|
||||
24
Streetwriters.Common/Models/PaddleResponse.cs
Normal file
24
Streetwriters.Common/Models/PaddleResponse.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Globalization;
|
||||
|
||||
public partial class PaddleResponse
|
||||
{
|
||||
[JsonPropertyName("error")]
|
||||
public PaddleError Error { get; set; }
|
||||
}
|
||||
|
||||
public class PaddleError
|
||||
{
|
||||
public string? Type { get; set; }
|
||||
public string? Code { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
[JsonPropertyName("documentation_url")]
|
||||
public string? DocumentationUrl { get; set; }
|
||||
}
|
||||
}
|
||||
20
Streetwriters.Common/Models/RefundPaymentResponse.cs
Normal file
20
Streetwriters.Common/Models/RefundPaymentResponse.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
public partial class RefundPaymentResponse
|
||||
{
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("response")]
|
||||
public Refund Refund { get; set; }
|
||||
}
|
||||
|
||||
public partial class Refund
|
||||
{
|
||||
[JsonPropertyName("refund_request_id")]
|
||||
public long RefundRequestId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ 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.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
@@ -28,5 +29,7 @@ namespace Streetwriters.Common.Models
|
||||
[JsonIgnore]
|
||||
public bool Success { get; set; }
|
||||
public int StatusCode { get; set; }
|
||||
[JsonIgnore]
|
||||
public HttpContent Content { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +1,103 @@
|
||||
/*
|
||||
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;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
public class Subscription : ISubscription
|
||||
{
|
||||
public Subscription()
|
||||
{
|
||||
Id = ObjectId.GenerateNewId().ToString();
|
||||
}
|
||||
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("userId")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string OrderId { get; set; }
|
||||
[JsonIgnore]
|
||||
public string SubscriptionId { get; set; }
|
||||
|
||||
[BsonRepresentation(BsonType.Int32)]
|
||||
[JsonPropertyName("appId")]
|
||||
public ApplicationType AppId { get; set; }
|
||||
|
||||
[JsonPropertyName("start")]
|
||||
public long StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("expiry")]
|
||||
public long ExpiryDate { get; set; }
|
||||
|
||||
[BsonRepresentation(BsonType.Int32)]
|
||||
[JsonPropertyName("provider")]
|
||||
public SubscriptionProvider Provider { get; set; }
|
||||
|
||||
[BsonRepresentation(BsonType.Int32)]
|
||||
[JsonPropertyName("type")]
|
||||
public SubscriptionType Type { get; set; }
|
||||
|
||||
[JsonPropertyName("cancelURL")]
|
||||
public string CancelURL { get; set; }
|
||||
|
||||
[JsonPropertyName("updateURL")]
|
||||
public string UpdateURL { get; set; }
|
||||
|
||||
[JsonPropertyName("productId")]
|
||||
public string ProductId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int TrialExtensionCount { get; set; }
|
||||
}
|
||||
}
|
||||
/*
|
||||
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.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
public class Subscription
|
||||
{
|
||||
public Subscription()
|
||||
{
|
||||
Id = ObjectId.GenerateNewId().ToString();
|
||||
}
|
||||
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("userId")]
|
||||
public required string UserId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string? OrderId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string? SubscriptionId { get; set; }
|
||||
|
||||
[BsonRepresentation(BsonType.Int32)]
|
||||
[JsonPropertyName("appId")]
|
||||
public required ApplicationType AppId { get; set; }
|
||||
|
||||
[JsonPropertyName("start")]
|
||||
public long StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("expiry")]
|
||||
public long ExpiryDate { get; set; }
|
||||
|
||||
[BsonRepresentation(BsonType.Int32)]
|
||||
[JsonPropertyName("provider")]
|
||||
public SubscriptionProvider Provider { get; set; }
|
||||
|
||||
[BsonRepresentation(BsonType.Int32)]
|
||||
[JsonPropertyName("type")]
|
||||
[Obsolete("Use SubscriptionPlan and SubscriptionStatus instead.")]
|
||||
public SubscriptionType Type { get; set; }
|
||||
|
||||
[JsonPropertyName("cancelURL")]
|
||||
public string? CancelURL { get; set; }
|
||||
|
||||
[JsonPropertyName("updateURL")]
|
||||
public string? UpdateURL { get; set; }
|
||||
|
||||
[JsonPropertyName("googlePurchaseToken")]
|
||||
public string? GooglePurchaseToken { get; set; }
|
||||
|
||||
[JsonPropertyName("productId")]
|
||||
public string? ProductId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int TrialExtensionCount { get; set; }
|
||||
|
||||
[JsonPropertyName("trialExpiry")]
|
||||
public long TrialExpiryDate { get; set; }
|
||||
|
||||
[JsonPropertyName("trialsAvailed")]
|
||||
public SubscriptionPlan[]? TrialsAvailed { get; set; }
|
||||
|
||||
[JsonPropertyName("updatedAt")]
|
||||
public long UpdatedAt { get; set; }
|
||||
|
||||
[BsonRepresentation(BsonType.Int32)]
|
||||
[JsonPropertyName("plan")]
|
||||
public SubscriptionPlan Plan { get; set; }
|
||||
|
||||
[BsonRepresentation(BsonType.Int32)]
|
||||
[JsonPropertyName("status")]
|
||||
public SubscriptionStatus Status { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
57
Streetwriters.Common/Models/SubscriptionPreviewResponse.cs
Normal file
57
Streetwriters.Common/Models/SubscriptionPreviewResponse.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Globalization;
|
||||
|
||||
public partial class SubscriptionPreviewResponse : PaddleResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public SubscriptionPreviewData Data { get; set; }
|
||||
}
|
||||
|
||||
public partial class SubscriptionPreviewData
|
||||
{
|
||||
[JsonPropertyName("currency_code")]
|
||||
public string CurrencyCode { get; set; }
|
||||
|
||||
[JsonPropertyName("billing_cycle")]
|
||||
public BillingCycle BillingCycle { get; set; }
|
||||
|
||||
[JsonPropertyName("update_summary")]
|
||||
public UpdateSummary UpdateSummary { get; set; }
|
||||
|
||||
[JsonPropertyName("immediate_transaction")]
|
||||
public TransactionV2 ImmediateTransaction { get; set; }
|
||||
|
||||
[JsonPropertyName("next_transaction")]
|
||||
public TransactionV2 NextTransaction { get; set; }
|
||||
|
||||
[JsonPropertyName("recurring_transaction_details")]
|
||||
public Details RecurringTransactionDetails { get; set; }
|
||||
}
|
||||
|
||||
public partial class UpdateSummary
|
||||
{
|
||||
[JsonPropertyName("charge")]
|
||||
public UpdateSummaryItem Charge { get; set; }
|
||||
|
||||
[JsonPropertyName("credit")]
|
||||
public UpdateSummaryItem Credit { get; set; }
|
||||
|
||||
[JsonPropertyName("result")]
|
||||
public UpdateSummaryItem Result { get; set; }
|
||||
}
|
||||
|
||||
public partial class UpdateSummaryItem
|
||||
{
|
||||
[JsonPropertyName("amount")]
|
||||
public long Amount { get; set; }
|
||||
|
||||
[JsonPropertyName("action")]
|
||||
public string? Action { get; set; }
|
||||
}
|
||||
}
|
||||
138
Streetwriters.Common/Services/PaddleBillingService.cs
Normal file
138
Streetwriters.Common/Services/PaddleBillingService.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Streetwriters.Common.Services
|
||||
{
|
||||
public class PaddleBillingService
|
||||
{
|
||||
#if DEBUG
|
||||
private const string PADDLE_BASE_URI = "https://sandbox-api.paddle.com";
|
||||
#else
|
||||
private const string PADDLE_BASE_URI = "https://api.paddle.com";
|
||||
#endif
|
||||
private readonly HttpClient httpClient = new();
|
||||
public PaddleBillingService(string paddleApiKey)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", paddleApiKey);
|
||||
}
|
||||
|
||||
public async Task<GetSubscriptionResponse?> GetSubscriptionAsync(string subscriptionId)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}";
|
||||
var response = await httpClient.GetAsync(url);
|
||||
return await response.Content.ReadFromJsonAsync<GetSubscriptionResponse>();
|
||||
}
|
||||
|
||||
public async Task<GetTransactionResponse?> GetTransactionAsync(string transactionId)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/transactions/{transactionId}";
|
||||
var response = await httpClient.GetAsync(url);
|
||||
return await response.Content.ReadFromJsonAsync<GetTransactionResponse>();
|
||||
}
|
||||
|
||||
public async Task<GetTransactionInvoiceResponse?> GetTransactionInvoiceAsync(string transactionId)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/transactions/{transactionId}/invoice";
|
||||
var response = await httpClient.GetAsync(url);
|
||||
return await response.Content.ReadFromJsonAsync<GetTransactionInvoiceResponse>();
|
||||
}
|
||||
|
||||
public async Task<ListTransactionsResponseV2?> ListTransactionsAsync(string? subscriptionId = null, string? customerId = null, string[]? status = null, string[]? origin = null)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/transactions";
|
||||
var parameters = new Dictionary<string, string?>()
|
||||
{
|
||||
{ "subscription_id", subscriptionId },
|
||||
{ "customer_id", customerId },
|
||||
{ "status", string.Join(',', status ?? ["billed","completed"]) },
|
||||
{ "order_by", "billed_at[DESC]" }
|
||||
};
|
||||
if (origin is not null) parameters.Add("origin", string.Join(',', origin));
|
||||
var response = await httpClient.GetAsync(QueryHelpers.AddQueryString(url, parameters));
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<ListTransactionsResponseV2>();
|
||||
}
|
||||
|
||||
public async Task<PaddleResponse?> RefundTransactionAsync(string transactionId, string transactionItemId, string reason = "")
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/adjustments";
|
||||
var response = await httpClient.PostAsync(url, JsonContent.Create(new Dictionary<string, object>
|
||||
{
|
||||
{ "action", "refund" },
|
||||
{
|
||||
"items",
|
||||
new object[]
|
||||
{
|
||||
new Dictionary<string, string> {
|
||||
{"item_id", transactionItemId},
|
||||
{"type", "full"}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "reason", reason },
|
||||
{ "transaction_id", transactionId }
|
||||
}));
|
||||
return await response.Content.ReadFromJsonAsync<PaddleResponse>();
|
||||
}
|
||||
|
||||
public async Task<SubscriptionPreviewResponse?> PreviewSubscriptionChangeAsync(string subscriptionId, string newProductId, bool isTrialing)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}/preview";
|
||||
var response = await httpClient.PatchAsync(url, JsonContent.Create(new
|
||||
{
|
||||
proration_billing_mode = isTrialing ? "do_not_bill" : "prorated_immediately",
|
||||
items = new[] { new { price_id = newProductId, quantity = 1 } }
|
||||
}));
|
||||
return await response.Content.ReadFromJsonAsync<SubscriptionPreviewResponse>();
|
||||
}
|
||||
|
||||
public async Task<PaddleResponse?> ChangeSubscriptionAsync(string subscriptionId, string newProductId, bool isTrialing)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}";
|
||||
var response = await httpClient.PatchAsync(url, JsonContent.Create(new
|
||||
{
|
||||
proration_billing_mode = isTrialing ? "do_not_bill" : "prorated_immediately",
|
||||
items = new[] { new { price_id = newProductId, quantity = 1 } }
|
||||
}));
|
||||
return await response.Content.ReadFromJsonAsync<PaddleResponse>();
|
||||
}
|
||||
|
||||
public async Task<PaddleResponse?> CancelSubscriptionAsync(string subscriptionId)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}/cancel";
|
||||
var response = await httpClient.PostAsync(url, JsonContent.Create(new { effective_from = "immediately" }));
|
||||
return await response.Content.ReadFromJsonAsync<PaddleResponse>();
|
||||
}
|
||||
|
||||
public async Task<PaddleResponse?> PauseSubscriptionAsync(string subscriptionId)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}/pause";
|
||||
var response = await httpClient.PostAsync(url, JsonContent.Create(new { }));
|
||||
return await response.Content.ReadFromJsonAsync<PaddleResponse>();
|
||||
}
|
||||
|
||||
public async Task<PaddleResponse?> ResumeSubscriptionAsync(string subscriptionId)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}";
|
||||
var response = await httpClient.PatchAsync(url, JsonContent.Create(new Dictionary<string, string?>
|
||||
{
|
||||
{"scheduled_change", null}
|
||||
}));
|
||||
return await response.Content.ReadFromJsonAsync<PaddleResponse>();
|
||||
}
|
||||
|
||||
public async Task<GetCustomerResponse?> FindCustomerFromTransactionAsync(string transactionId)
|
||||
{
|
||||
var transaction = await GetTransactionAsync(transactionId);
|
||||
if (transaction == null) return null;
|
||||
var url = $"{PADDLE_BASE_URI}/customers/{transaction.Transaction.CustomerId}";
|
||||
var response = await httpClient.GetFromJsonAsync<GetCustomerResponse>(url);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
188
Streetwriters.Common/Services/PaddleService.cs
Normal file
188
Streetwriters.Common/Services/PaddleService.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Streetwriters.Common.Services
|
||||
{
|
||||
public class PaddleService(string vendorId, string vendorAuthCode)
|
||||
{
|
||||
#if (DEBUG || STAGING)
|
||||
const string PADDLE_BASE_URI = "https://sandbox-vendors.paddle.com/api";
|
||||
#else
|
||||
const string PADDLE_BASE_URI = "https://vendors.paddle.com/api";
|
||||
#endif
|
||||
|
||||
HttpClient httpClient = new HttpClient();
|
||||
|
||||
public async Task<ListUsersResponse> ListUsersAsync(
|
||||
string subscriptionId,
|
||||
int results
|
||||
)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/2.0/subscription/users";
|
||||
var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync(
|
||||
url,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "vendor_id", vendorId },
|
||||
{ "vendor_auth_code", vendorAuthCode },
|
||||
{ "subscription_id", subscriptionId },
|
||||
{ "results_per_page", results.ToString() },
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<ListUsersResponse>();
|
||||
}
|
||||
|
||||
public async Task<ListPaymentsResponse> ListPaymentsAsync(
|
||||
string subscriptionId,
|
||||
long planId
|
||||
)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/2.0/subscription/payments";
|
||||
var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync(
|
||||
url,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "vendor_id", vendorId },
|
||||
{ "vendor_auth_code", vendorAuthCode },
|
||||
{ "subscription_id", subscriptionId },
|
||||
{ "is_paid", "1" },
|
||||
{ "plan", planId.ToString() },
|
||||
{ "is_one_off_charge", "0" },
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<ListPaymentsResponse>();
|
||||
}
|
||||
|
||||
public async Task<ListTransactionsResponse> ListTransactionsAsync(
|
||||
string subscriptionId
|
||||
)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/2.0/subscription/{subscriptionId}/transactions";
|
||||
var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync(
|
||||
url,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "vendor_id", vendorId },
|
||||
{ "vendor_auth_code", vendorAuthCode },
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<ListTransactionsResponse>();
|
||||
}
|
||||
|
||||
public async Task<PaddleTransactionUser> FindUserFromOrderAsync(string orderId)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/2.0/order/{orderId}/transactions";
|
||||
var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync(
|
||||
url,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "vendor_id", vendorId },
|
||||
{ "vendor_auth_code", vendorAuthCode },
|
||||
}
|
||||
)
|
||||
);
|
||||
var transactions = await response.Content.ReadFromJsonAsync<ListTransactionsResponse>();
|
||||
if (transactions.Transactions.Length == 0) return null;
|
||||
return transactions.Transactions[0].User;
|
||||
}
|
||||
|
||||
public async Task<bool> RefundPaymentAsync(string paymentId, string reason = "")
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/2.0/payment/refund";
|
||||
var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync(
|
||||
url,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "vendor_id", vendorId },
|
||||
{ "vendor_auth_code", vendorAuthCode },
|
||||
{ "order_id", paymentId },
|
||||
{ "reason", reason },
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
var refundResponse = await response.Content.ReadFromJsonAsync<RefundPaymentResponse>();
|
||||
return refundResponse.Success;
|
||||
}
|
||||
|
||||
public async Task<bool> CancelSubscriptionAsync(string subscriptionId)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/2.0/subscription/users_cancel";
|
||||
var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync(
|
||||
url,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "vendor_id", vendorId },
|
||||
{ "vendor_auth_code", vendorAuthCode },
|
||||
{ "subscription_id", subscriptionId },
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task<bool> PauseSubscriptionAsync(string subscriptionId)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/2.0/subscription/users/update";
|
||||
var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync(
|
||||
url,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "vendor_id", vendorId },
|
||||
{ "vendor_auth_code", vendorAuthCode },
|
||||
{ "subscription_id", subscriptionId },
|
||||
{ "pause", "true" },
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task<bool> ResumeSubscriptionAsync(string subscriptionId)
|
||||
{
|
||||
var url = $"{PADDLE_BASE_URI}/2.0/subscription/users/update";
|
||||
var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync(
|
||||
url,
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "vendor_id", vendorId },
|
||||
{ "vendor_auth_code", vendorAuthCode },
|
||||
{ "subscription_id", subscriptionId },
|
||||
{ "pause", "false" },
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Streetwriters.Common/Services/URLAnalyzer.cs
Normal file
36
Streetwriters.Common/Services/URLAnalyzer.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
|
||||
namespace Streetwriters.Common.Services
|
||||
{
|
||||
struct Threat
|
||||
{
|
||||
public string[]? ThreatTypes { get; set; }
|
||||
}
|
||||
struct WebRiskAPIResponse
|
||||
{
|
||||
public Threat Threat { get; set; }
|
||||
}
|
||||
|
||||
public class URLAnalyzer : IURLAnalyzer, IDisposable
|
||||
{
|
||||
private readonly HttpClient httpClient = new();
|
||||
|
||||
public async Task<bool> IsURLSafeAsync(string uri)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Constants.WEBRISK_API_URI)) return true;
|
||||
var response = await httpClient.PostAsJsonAsync(Constants.WEBRISK_API_URI, new { uri });
|
||||
if (!response.IsSuccessStatusCode) return true;
|
||||
var json = await response.Content.ReadFromJsonAsync<WebRiskAPIResponse>();
|
||||
return json.Threat.ThreatTypes == null || json.Threat.ThreatTypes.Length == 0;
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
|
||||
@@ -1,125 +1,140 @@
|
||||
/*
|
||||
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.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using Streetwriters.Common.Helpers;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using WampSharp.V2.Client;
|
||||
|
||||
namespace Streetwriters.Common
|
||||
{
|
||||
public class WampServer<T> where T : new()
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, IWampRealmProxy> Channels = new();
|
||||
|
||||
public string Endpoint { get; set; }
|
||||
public string Address { get; set; }
|
||||
public T Topics { get; set; } = new T();
|
||||
public string Realm { get; set; }
|
||||
|
||||
private async Task<IWampRealmProxy> GetChannelAsync(string topic)
|
||||
{
|
||||
if (!Channels.TryGetValue(topic, out IWampRealmProxy channel) || !channel.Monitor.IsConnected)
|
||||
{
|
||||
channel = await WampHelper.OpenWampChannelAsync(Address, Realm);
|
||||
Channels.AddOrUpdate(topic, (key) => channel, (key, old) => channel);
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
public async Task<V> GetServiceAsync<V>(string topic) where V : class
|
||||
{
|
||||
var channel = await GetChannelAsync(topic);
|
||||
return channel.Services.GetCalleeProxy<V>();
|
||||
}
|
||||
|
||||
public async Task PublishMessageAsync<V>(string topic, V message)
|
||||
{
|
||||
try
|
||||
{
|
||||
IWampRealmProxy channel = await GetChannelAsync(topic);
|
||||
WampHelper.PublishMessage(channel, topic, message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Slogger<WampServer<T>>.Error(nameof(PublishMessageAsync), ex.ToString());
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class WampServers
|
||||
{
|
||||
public static WampServer<MessengerServerTopics> MessengerServer { get; } = new WampServer<MessengerServerTopics>
|
||||
{
|
||||
Endpoint = "/wamp",
|
||||
Address = $"{Servers.MessengerServer.WS()}/wamp",
|
||||
Realm = "messages",
|
||||
};
|
||||
|
||||
public static WampServer<SubscriptionServerTopics> SubscriptionServer { get; } = new WampServer<SubscriptionServerTopics>
|
||||
{
|
||||
Endpoint = "/wamp",
|
||||
Address = $"{Servers.SubscriptionServer.WS()}/wamp",
|
||||
Realm = "messages",
|
||||
};
|
||||
|
||||
public static WampServer<IdentityServerTopics> IdentityServer { get; } = new WampServer<IdentityServerTopics>
|
||||
{
|
||||
Endpoint = "/wamp",
|
||||
Address = $"{Servers.IdentityServer.WS()}/wamp",
|
||||
Realm = "messages",
|
||||
};
|
||||
|
||||
public static WampServer<NotesnookServerTopics> NotesnookServer { get; } = new WampServer<NotesnookServerTopics>
|
||||
{
|
||||
Endpoint = "/wamp",
|
||||
Address = $"{Servers.NotesnookAPI.WS()}/wamp",
|
||||
Realm = "messages",
|
||||
};
|
||||
}
|
||||
|
||||
public class MessengerServerTopics
|
||||
{
|
||||
public const string SendSSETopic = "co.streetwriters.sse.send";
|
||||
}
|
||||
|
||||
public class SubscriptionServerTopics
|
||||
{
|
||||
public const string UserSubscriptionServiceTopic = "co.streetwriters.subscriptions.subscriptions";
|
||||
|
||||
public const string CreateSubscriptionTopic = "co.streetwriters.subscriptions.create";
|
||||
public const string DeleteSubscriptionTopic = "co.streetwriters.subscriptions.delete";
|
||||
}
|
||||
|
||||
public class IdentityServerTopics
|
||||
{
|
||||
public const string UserAccountServiceTopic = "co.streetwriters.identity.users";
|
||||
public const string ClearCacheTopic = "co.streetwriters.identity.clear_cache";
|
||||
public const string DeleteUserTopic = "co.streetwriters.identity.delete_user";
|
||||
}
|
||||
|
||||
public class NotesnookServerTopics
|
||||
{
|
||||
}
|
||||
/*
|
||||
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.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using Streetwriters.Common.Helpers;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using WampSharp.V2.Client;
|
||||
|
||||
namespace Streetwriters.Common
|
||||
{
|
||||
public class WampServer<T> where T : new()
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, IWampRealmProxy> Channels = new();
|
||||
|
||||
public string Endpoint { get; set; }
|
||||
public string Address { get; set; }
|
||||
public T Topics { get; set; } = new T();
|
||||
public string Realm { get; set; }
|
||||
|
||||
private async Task<IWampRealmProxy> GetChannelAsync(string topic)
|
||||
{
|
||||
if (!Channels.TryGetValue(topic, out IWampRealmProxy channel) || !channel.Monitor.IsConnected)
|
||||
{
|
||||
channel = await WampHelper.OpenWampChannelAsync(Address, Realm);
|
||||
Channels.AddOrUpdate(topic, (key) => channel, (key, old) => channel);
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
public async Task<V> GetServiceAsync<V>(string topic) where V : class
|
||||
{
|
||||
var channel = await GetChannelAsync(topic);
|
||||
return channel.Services.GetCalleeProxy<V>();
|
||||
}
|
||||
|
||||
public async Task PublishMessageAsync<V>(string topic, V message)
|
||||
{
|
||||
try
|
||||
{
|
||||
IWampRealmProxy channel = await GetChannelAsync(topic);
|
||||
WampHelper.PublishMessage(channel, topic, message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Slogger<WampServer<T>>.Error(nameof(PublishMessageAsync), ex.ToString());
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PublishMessagesAsync<V>(string topic, IEnumerable<V> messages)
|
||||
{
|
||||
try
|
||||
{
|
||||
IWampRealmProxy channel = await GetChannelAsync(topic);
|
||||
WampHelper.PublishMessages(channel, topic, messages);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Slogger<WampServer<T>>.Error(nameof(PublishMessagesAsync), ex.ToString());
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class WampServers
|
||||
{
|
||||
public static WampServer<MessengerServerTopics> MessengerServer { get; } = new WampServer<MessengerServerTopics>
|
||||
{
|
||||
Endpoint = "/wamp",
|
||||
Address = $"{Servers.MessengerServer.WS()}/wamp",
|
||||
Realm = "messages",
|
||||
};
|
||||
|
||||
public static WampServer<SubscriptionServerTopics> SubscriptionServer { get; } = new WampServer<SubscriptionServerTopics>
|
||||
{
|
||||
Endpoint = "/wamp",
|
||||
Address = $"{Servers.SubscriptionServer.WS()}/wamp",
|
||||
Realm = "messages",
|
||||
};
|
||||
|
||||
public static WampServer<IdentityServerTopics> IdentityServer { get; } = new WampServer<IdentityServerTopics>
|
||||
{
|
||||
Endpoint = "/wamp",
|
||||
Address = $"{Servers.IdentityServer.WS()}/wamp",
|
||||
Realm = "messages",
|
||||
};
|
||||
|
||||
public static WampServer<NotesnookServerTopics> NotesnookServer { get; } = new WampServer<NotesnookServerTopics>
|
||||
{
|
||||
Endpoint = "/wamp",
|
||||
Address = $"{Servers.NotesnookAPI.WS()}/wamp",
|
||||
Realm = "messages",
|
||||
};
|
||||
}
|
||||
|
||||
public class MessengerServerTopics
|
||||
{
|
||||
public const string SendSSETopic = "co.streetwriters.sse.send";
|
||||
}
|
||||
|
||||
public class SubscriptionServerTopics
|
||||
{
|
||||
public const string UserSubscriptionServiceTopic = "co.streetwriters.subscriptions.subscriptions";
|
||||
|
||||
public const string CreateSubscriptionTopic = "co.streetwriters.subscriptions.create";
|
||||
public const string CreateSubscriptionV2Topic = "co.streetwriters.subscriptions.v2.create";
|
||||
public const string DeleteSubscriptionTopic = "co.streetwriters.subscriptions.delete";
|
||||
}
|
||||
|
||||
public class IdentityServerTopics
|
||||
{
|
||||
public const string UserAccountServiceTopic = "co.streetwriters.identity.users";
|
||||
public const string ClearCacheTopic = "co.streetwriters.identity.clear_cache";
|
||||
public const string DeleteUserTopic = "co.streetwriters.identity.delete_user";
|
||||
}
|
||||
|
||||
public class NotesnookServerTopics
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,11 @@ namespace Streetwriters.Data.Repositories
|
||||
return all.ToList();
|
||||
}
|
||||
|
||||
public virtual async Task<long> CountAsync(Expression<Func<TEntity, bool>> filterExpression)
|
||||
{
|
||||
return await Collection.CountDocumentsAsync(filterExpression);
|
||||
}
|
||||
|
||||
public virtual void Update(string id, TEntity obj)
|
||||
{
|
||||
dbContext.AddCommand((handle, ct) => Collection.ReplaceOneAsync(handle, Builders<TEntity>.Filter.Eq("_id", ObjectId.Parse(id)), obj, cancellationToken: ct));
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace Streetwriters.Identity.Controllers
|
||||
[HttpPost("send")]
|
||||
[Authorize("mfa")]
|
||||
[Authorize(LocalApi.PolicyName)]
|
||||
[EnableRateLimiting("strict")]
|
||||
[EnableRateLimiting("super_strict")]
|
||||
public async Task<IActionResult> RequestCode([FromForm] string type)
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Streetwriters.Identity.Controllers
|
||||
await UserManager.AddToRoleAsync(user, client.Id);
|
||||
if (Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
await UserManager.AddClaimAsync(user, UserService.SubscriptionTypeToClaim(client.Id, Common.Enums.SubscriptionType.PREMIUM));
|
||||
await UserManager.AddClaimAsync(user, UserService.SubscriptionPlanToClaim(client.Id, SubscriptionPlan.BELIEVER));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,49 +1,50 @@
|
||||
/*
|
||||
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.Threading.Tasks;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Security.Claims;
|
||||
using System.Linq;
|
||||
using Streetwriters.Identity.Services;
|
||||
|
||||
namespace Streetwriters.Identity.MessageHandlers
|
||||
{
|
||||
public class CreateSubscription
|
||||
{
|
||||
public static async Task Process(CreateSubscriptionMessage message, UserManager<User> userManager)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(message.UserId);
|
||||
var client = Clients.FindClientByAppId(message.AppId);
|
||||
if (client == null || user == null) return;
|
||||
|
||||
IdentityUserClaim<string> statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == UserService.GetClaimKey(client.Id));
|
||||
Claim subscriptionClaim = UserService.SubscriptionTypeToClaim(client.Id, message.Type);
|
||||
if (statusClaim?.ClaimValue == subscriptionClaim.Value) return;
|
||||
if (statusClaim != null)
|
||||
await userManager.ReplaceClaimAsync(user, statusClaim.ToClaim(), subscriptionClaim);
|
||||
else
|
||||
await userManager.AddClaimAsync(user, subscriptionClaim);
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
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.Threading.Tasks;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Security.Claims;
|
||||
using System.Linq;
|
||||
using Streetwriters.Identity.Services;
|
||||
|
||||
namespace Streetwriters.Identity.MessageHandlers
|
||||
{
|
||||
public class CreateSubscription
|
||||
{
|
||||
public static async Task Process(CreateSubscriptionMessage message, UserManager<User> userManager)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(message.UserId);
|
||||
var client = Clients.FindClientByAppId(message.AppId);
|
||||
if (client == null || user == null) return;
|
||||
|
||||
IdentityUserClaim<string> statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == UserService.GetClaimKey(client.Id));
|
||||
Claim subscriptionClaim = UserService.SubscriptionTypeToClaim(client.Id, message.Type);
|
||||
if (statusClaim?.ClaimValue == subscriptionClaim.Value) return;
|
||||
if (statusClaim != null)
|
||||
await userManager.ReplaceClaimAsync(user, statusClaim.ToClaim(), subscriptionClaim);
|
||||
// we no longer accept legacy subscriptions.
|
||||
// else
|
||||
// await userManager.AddClaimAsync(user, subscriptionClaim);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
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.Threading.Tasks;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Security.Claims;
|
||||
using System.Linq;
|
||||
using Streetwriters.Identity.Services;
|
||||
|
||||
namespace Streetwriters.Identity.MessageHandlers
|
||||
{
|
||||
public class CreateSubscriptionV2
|
||||
{
|
||||
public static async Task Process(CreateSubscriptionMessageV2 message, UserManager<User> userManager)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(message.UserId);
|
||||
var client = Clients.FindClientByAppId(message.AppId);
|
||||
if (client == null || user == null) return;
|
||||
|
||||
IdentityUserClaim<string> statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == UserService.GetClaimKey(client.Id));
|
||||
Claim subscriptionClaim = UserService.SubscriptionPlanToClaim(client.Id, message.Plan);
|
||||
if (statusClaim?.ClaimValue == subscriptionClaim.Value) return;
|
||||
if (statusClaim != null)
|
||||
await userManager.ReplaceClaimAsync(user, statusClaim.ToClaim(), subscriptionClaim);
|
||||
else
|
||||
await userManager.AddClaimAsync(user, subscriptionClaim);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ namespace Streetwriters.Identity.MessageHandlers
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(message.UserId);
|
||||
var client = Clients.FindClientByAppId(message.AppId);
|
||||
if (client != null || user != null) return;
|
||||
if (client == null || user == null) return;
|
||||
|
||||
IdentityUserClaim<string> statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == $"{client.Id}:status");
|
||||
if (statusClaim != null)
|
||||
|
||||
@@ -24,6 +24,7 @@ using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Models;
|
||||
@@ -170,8 +171,8 @@ namespace Streetwriters.Identity.Services
|
||||
|
||||
if (isSetup &&
|
||||
method == MFAMethods.SMS &&
|
||||
!UserService.IsUserPremium(client.Id, user))
|
||||
throw new Exception("Due to the high costs of SMS, currently 2FA via SMS is only available for Pro users.");
|
||||
!UserService.IsSMSMFAAllowed(client.Id, user))
|
||||
throw new Exception("Due to the high costs of SMS, 2FA via SMS is only available on Pro & Believer plans.");
|
||||
|
||||
// if (!user.EmailConfirmed) throw new Exception("Please confirm your email before activating 2FA by email.");
|
||||
await GetAuthenticatorDetailsAsync(user, client);
|
||||
@@ -185,6 +186,7 @@ namespace Streetwriters.Identity.Services
|
||||
case "sms":
|
||||
await UserManager.SetPhoneNumberAsync(user, form.PhoneNumber);
|
||||
var id = await SMSSender.SendOTPAsync(form.PhoneNumber, client);
|
||||
await Slogger<MFAService>.Info("SendOTPAsync", user.Id.ToString(), id);
|
||||
await this.ReplaceClaimAsync(user, MFAService.SMS_ID_CLAIM, id);
|
||||
break;
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ namespace Streetwriters.Identity.Services
|
||||
{
|
||||
public class UserAccountService(UserManager<User> userManager, IMFAService mfaService) : IUserAccountService
|
||||
{
|
||||
public async Task<UserModel> GetUserAsync(string clientId, string userId)
|
||||
public async Task<UserModel?> GetUserAsync(string clientId, string userId)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId);
|
||||
if (!await UserService.IsUserValidAsync(userManager, user, clientId))
|
||||
throw new Exception($"Unable to find user with ID '{userId}'.");
|
||||
return null;
|
||||
|
||||
var claims = await userManager.GetClaimsAsync(user);
|
||||
var marketingConsentClaim = claims.FirstOrDefault((claim) => claim.Type == $"{clientId}:marketing_consent");
|
||||
@@ -46,7 +46,7 @@ namespace Streetwriters.Identity.Services
|
||||
public async Task DeleteUserAsync(string clientId, string userId, string password)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId);
|
||||
if (!await UserService.IsUserValidAsync(userManager, user, clientId)) throw new Exception($"User not found.");
|
||||
if (!await UserService.IsUserValidAsync(userManager, user, clientId)) return;
|
||||
|
||||
if (!await userManager.CheckPasswordAsync(user, password)) throw new Exception("Wrong password.");
|
||||
|
||||
|
||||
@@ -1,89 +1,133 @@
|
||||
/*
|
||||
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.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Streetwriters.Identity.Services
|
||||
{
|
||||
public class UserService
|
||||
{
|
||||
public static SubscriptionType GetUserSubscriptionStatus(string clientId, User user)
|
||||
{
|
||||
var claimKey = GetClaimKey(clientId);
|
||||
var status = user.Claims.FirstOrDefault((c) => c.ClaimType == claimKey).ClaimValue;
|
||||
switch (status)
|
||||
{
|
||||
case "basic":
|
||||
return SubscriptionType.BASIC;
|
||||
case "trial":
|
||||
return SubscriptionType.TRIAL;
|
||||
case "premium":
|
||||
return SubscriptionType.PREMIUM;
|
||||
case "premium_canceled":
|
||||
return SubscriptionType.PREMIUM_CANCELED;
|
||||
case "premium_expired":
|
||||
return SubscriptionType.PREMIUM_EXPIRED;
|
||||
default:
|
||||
return SubscriptionType.BASIC;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static bool IsUserPremium(string clientId, User user)
|
||||
{
|
||||
var status = GetUserSubscriptionStatus(clientId, user);
|
||||
return status == SubscriptionType.PREMIUM || status == SubscriptionType.PREMIUM_CANCELED;
|
||||
}
|
||||
|
||||
public static Claim SubscriptionTypeToClaim(string clientId, SubscriptionType type)
|
||||
{
|
||||
var claimKey = GetClaimKey(clientId);
|
||||
switch (type)
|
||||
{
|
||||
case SubscriptionType.BASIC:
|
||||
return new Claim(claimKey, "basic");
|
||||
case SubscriptionType.TRIAL:
|
||||
return new Claim(claimKey, "trial");
|
||||
case SubscriptionType.PREMIUM:
|
||||
return new Claim(claimKey, "premium");
|
||||
case SubscriptionType.PREMIUM_CANCELED:
|
||||
return new Claim(claimKey, "premium_canceled");
|
||||
case SubscriptionType.PREMIUM_EXPIRED:
|
||||
return new Claim(claimKey, "premium_expired");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetClaimKey(string clientId)
|
||||
{
|
||||
return $"{clientId}:status";
|
||||
}
|
||||
|
||||
public static async Task<bool> IsUserValidAsync(UserManager<User> userManager, User user, string clientId)
|
||||
{
|
||||
return user != null && await userManager.IsInRoleAsync(user, clientId);
|
||||
}
|
||||
}
|
||||
/*
|
||||
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.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Streetwriters.Identity.Services
|
||||
{
|
||||
public class UserService
|
||||
{
|
||||
private static SubscriptionType? GetUserSubscriptionStatus(string clientId, User user)
|
||||
{
|
||||
var claimKey = GetClaimKey(clientId);
|
||||
var status = user.Claims.FirstOrDefault((c) => c.ClaimType == claimKey).ClaimValue;
|
||||
switch (status)
|
||||
{
|
||||
case "basic":
|
||||
return SubscriptionType.BASIC;
|
||||
case "trial":
|
||||
return SubscriptionType.TRIAL;
|
||||
case "premium":
|
||||
return SubscriptionType.PREMIUM;
|
||||
case "premium_canceled":
|
||||
return SubscriptionType.PREMIUM_CANCELED;
|
||||
case "premium_expired":
|
||||
return SubscriptionType.PREMIUM_EXPIRED;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static SubscriptionPlan? GetUserSubscriptionPlan(string clientId, User user)
|
||||
{
|
||||
var claimKey = GetClaimKey(clientId);
|
||||
var status = user.Claims.FirstOrDefault((c) => c.ClaimType == claimKey).ClaimValue;
|
||||
switch (status)
|
||||
{
|
||||
case "free":
|
||||
return SubscriptionPlan.FREE;
|
||||
case "believer":
|
||||
return SubscriptionPlan.BELIEVER;
|
||||
case "education":
|
||||
return SubscriptionPlan.EDUCATION;
|
||||
case "essential":
|
||||
return SubscriptionPlan.ESSENTIAL;
|
||||
case "pro":
|
||||
return SubscriptionPlan.PRO;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsSMSMFAAllowed(string clientId, User user)
|
||||
{
|
||||
var legacyStatus = GetUserSubscriptionStatus(clientId, user);
|
||||
var status = GetUserSubscriptionPlan(clientId, user);
|
||||
if (legacyStatus == null && status == null) return false;
|
||||
return legacyStatus == SubscriptionType.PREMIUM ||
|
||||
legacyStatus == SubscriptionType.PREMIUM_CANCELED ||
|
||||
status == SubscriptionPlan.PRO ||
|
||||
status == SubscriptionPlan.EDUCATION ||
|
||||
status == SubscriptionPlan.BELIEVER;
|
||||
}
|
||||
|
||||
public static Claim SubscriptionTypeToClaim(string clientId, SubscriptionType type)
|
||||
{
|
||||
var claimKey = GetClaimKey(clientId);
|
||||
switch (type)
|
||||
{
|
||||
case SubscriptionType.BASIC:
|
||||
return new Claim(claimKey, "basic");
|
||||
case SubscriptionType.TRIAL:
|
||||
return new Claim(claimKey, "trial");
|
||||
case SubscriptionType.PREMIUM:
|
||||
return new Claim(claimKey, "premium");
|
||||
case SubscriptionType.PREMIUM_CANCELED:
|
||||
return new Claim(claimKey, "premium_canceled");
|
||||
case SubscriptionType.PREMIUM_EXPIRED:
|
||||
return new Claim(claimKey, "premium_expired");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Claim SubscriptionPlanToClaim(string clientId, SubscriptionPlan plan)
|
||||
{
|
||||
var claimKey = GetClaimKey(clientId);
|
||||
switch (plan)
|
||||
{
|
||||
case SubscriptionPlan.FREE:
|
||||
return new Claim(claimKey, "free");
|
||||
case SubscriptionPlan.BELIEVER:
|
||||
return new Claim(claimKey, "believer");
|
||||
case SubscriptionPlan.EDUCATION:
|
||||
return new Claim(claimKey, "education");
|
||||
case SubscriptionPlan.ESSENTIAL:
|
||||
return new Claim(claimKey, "essential");
|
||||
case SubscriptionPlan.PRO:
|
||||
return new Claim(claimKey, "pro");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetClaimKey(string clientId)
|
||||
{
|
||||
return $"{clientId}:status";
|
||||
}
|
||||
|
||||
public static async Task<bool> IsUserValidAsync(UserManager<User> userManager, User user, string clientId)
|
||||
{
|
||||
return user != null && await userManager.IsInRoleAsync(user, clientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.RateLimiting;
|
||||
using AspNetCore.Identity.Mongo;
|
||||
using IdentityServer4.MongoDB.Entities;
|
||||
@@ -31,6 +32,7 @@ using IdentityServer4.Stores;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
@@ -137,6 +139,7 @@ namespace Streetwriters.Identity
|
||||
|
||||
services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
options.AddSlidingWindowLimiter("strict", options =>
|
||||
{
|
||||
options.PermitLimit = 30;
|
||||
@@ -145,17 +148,27 @@ namespace Streetwriters.Identity
|
||||
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||
options.QueueLimit = 0;
|
||||
});
|
||||
});
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("mfa", policy =>
|
||||
options.AddPolicy("super_strict", (context) =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes("Bearer+jwt");
|
||||
policy.RequireClaim("scope", Config.MFA_GRANT_TYPE_SCOPE);
|
||||
var key = context.User?.FindFirstValue("sub") ?? "default";
|
||||
return RateLimitPartition.GetSlidingWindowLimiter(key, (key) => new SlidingWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = 6,
|
||||
Window = TimeSpan.FromMinutes(1),
|
||||
SegmentsPerWindow = 2,
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||
QueueLimit = int.MaxValue,
|
||||
AutoReplenishment = true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
services.AddAuthorizationBuilder().AddPolicy("mfa", policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes("Bearer+jwt");
|
||||
policy.RequireClaim("scope", Config.MFA_GRANT_TYPE_SCOPE);
|
||||
});
|
||||
|
||||
services.AddLocalApiAuthentication();
|
||||
services.AddAuthentication()
|
||||
@@ -166,7 +179,7 @@ namespace Streetwriters.Identity
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidTypes = new[] { "at+jwt" },
|
||||
ValidTypes = ["at+jwt"],
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateIssuer = true,
|
||||
@@ -231,6 +244,17 @@ namespace Streetwriters.Identity
|
||||
await MessageHandlers.CreateSubscription.Process(message, userManager);
|
||||
}
|
||||
});
|
||||
|
||||
realm.Subscribe(SubscriptionServerTopics.CreateSubscriptionV2Topic, async (CreateSubscriptionMessageV2 message) =>
|
||||
{
|
||||
using (var serviceScope = app.ApplicationServices.CreateScope())
|
||||
{
|
||||
var services = serviceScope.ServiceProvider;
|
||||
var userManager = services.GetRequiredService<UserManager<User>>();
|
||||
await MessageHandlers.CreateSubscriptionV2.Process(message, userManager);
|
||||
}
|
||||
});
|
||||
|
||||
realm.Subscribe(SubscriptionServerTopics.DeleteSubscriptionTopic, async (DeleteSubscriptionMessage message) =>
|
||||
{
|
||||
using (var serviceScope = app.ApplicationServices.CreateScope())
|
||||
|
||||
@@ -59,30 +59,11 @@ services:
|
||||
validate:
|
||||
condition: service_completed_successfully
|
||||
healthcheck:
|
||||
test: echo 'db.runCommand("ping").ok' | mongosh mongodb://localhost:27017 --quiet
|
||||
test: echo 'try { rs.status() } catch (err) { rs.initiate() }; db.runCommand("ping").ok' | mongosh mongodb://localhost:27017 --quiet
|
||||
interval: 40s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
# the notesnook sync server requires transactions which only work
|
||||
# with a MongoDB replica set.
|
||||
# This job just runs `rs.initiate()` on our mongodb instance
|
||||
# upgrading it to a replica set. This is only required once but we running
|
||||
# it multiple times is no issue.
|
||||
initiate-rs0:
|
||||
image: mongo:7.0.12
|
||||
networks:
|
||||
- notesnook
|
||||
depends_on:
|
||||
- notesnook-db
|
||||
entrypoint: /bin/sh
|
||||
command:
|
||||
- -c
|
||||
- |
|
||||
mongosh mongodb://notesnook-db:27017 <<EOF
|
||||
rs.initiate();
|
||||
rs.status();
|
||||
EOF
|
||||
|
||||
notesnook-s3:
|
||||
image: minio/minio:RELEASE.2024-07-29T22-14-52Z
|
||||
@@ -231,3 +212,4 @@ networks:
|
||||
volumes:
|
||||
dbdata:
|
||||
s3data:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user