mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-08-13 20:10:18 +02:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce7f02646 | ||
|
|
a1dbd3f8b8 | ||
|
|
99489b9b4c | ||
|
|
1b953f756e | ||
|
|
d27ab68735 | ||
|
|
294d885dbf | ||
|
|
bdd5017394 | ||
|
|
7ad70c63ee | ||
|
|
0367ab6f80 | ||
|
|
f3bfe0957b | ||
|
|
7f614f6954 | ||
|
|
6663778e3e | ||
|
|
14f0a3b37e | ||
|
|
82a1152f9f | ||
|
|
580524b855 | ||
|
|
159fe0e376 | ||
|
|
30fdaae36c | ||
|
|
815c8fb84c | ||
|
|
04b0c305ed | ||
|
|
31c57f95b2 |
@@ -42,6 +42,7 @@ namespace Notesnook.API.Accessors
|
|||||||
public SyncItemsRepository Colors { get; }
|
public SyncItemsRepository Colors { get; }
|
||||||
public SyncItemsRepository Vaults { get; }
|
public SyncItemsRepository Vaults { get; }
|
||||||
public SyncItemsRepository Tags { get; }
|
public SyncItemsRepository Tags { get; }
|
||||||
|
public SyncItemsRepository InboxItemsHistory { get; }
|
||||||
public Repository<UserSettings> UsersSettings { get; }
|
public Repository<UserSettings> UsersSettings { get; }
|
||||||
public Repository<Monograph> Monographs { get; }
|
public Repository<Monograph> Monographs { get; }
|
||||||
public Repository<InboxApiKey> InboxApiKey { get; }
|
public Repository<InboxApiKey> InboxApiKey { get; }
|
||||||
@@ -75,6 +76,8 @@ namespace Notesnook.API.Accessors
|
|||||||
IMongoCollection<SyncItem> vaults,
|
IMongoCollection<SyncItem> vaults,
|
||||||
[FromKeyedServices(Collections.TagsKey)]
|
[FromKeyedServices(Collections.TagsKey)]
|
||||||
IMongoCollection<SyncItem> tags,
|
IMongoCollection<SyncItem> tags,
|
||||||
|
[FromKeyedServices(Collections.InboxItemsHistoryKey)]
|
||||||
|
IMongoCollection<SyncItem> inboxItemsHistory,
|
||||||
|
|
||||||
Repository<UserSettings> usersSettings,
|
Repository<UserSettings> usersSettings,
|
||||||
Repository<Monograph> monographs,
|
Repository<Monograph> monographs,
|
||||||
@@ -102,6 +105,7 @@ namespace Notesnook.API.Accessors
|
|||||||
Colors = new SyncItemsRepository(dbContext, colors, logger);
|
Colors = new SyncItemsRepository(dbContext, colors, logger);
|
||||||
Vaults = new SyncItemsRepository(dbContext, vaults, logger);
|
Vaults = new SyncItemsRepository(dbContext, vaults, logger);
|
||||||
Tags = new SyncItemsRepository(dbContext, tags, logger);
|
Tags = new SyncItemsRepository(dbContext, tags, logger);
|
||||||
|
InboxItemsHistory = new SyncItemsRepository(dbContext, inboxItemsHistory, logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,5 +18,6 @@ namespace Notesnook.API
|
|||||||
public const string InboxApiKeysKey = "inbox_api_keys";
|
public const string InboxApiKeysKey = "inbox_api_keys";
|
||||||
public const string SyncDevicesKey = "sync_devices";
|
public const string SyncDevicesKey = "sync_devices";
|
||||||
public const string DeviceIdsChunksKey = "device_ids_chunks";
|
public const string DeviceIdsChunksKey = "device_ids_chunks";
|
||||||
|
public const string InboxItemsHistoryKey = "inbox_items_history";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -72,9 +73,9 @@ namespace Notesnook.API.Controllers
|
|||||||
{
|
{
|
||||||
return BadRequest(new { error = "Api key name is required." });
|
return BadRequest(new { error = "Api key name is required." });
|
||||||
}
|
}
|
||||||
if (request.ExpiryDate <= -1)
|
if (request.ExpiryDate == null)
|
||||||
{
|
{
|
||||||
return BadRequest(new { error = "Valid expiry date is required." });
|
return BadRequest(new { error = "Expiry date is required." });
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = await inboxApiKeysRepository.CountAsync(t => t.UserId == userId);
|
var count = await inboxApiKeysRepository.CountAsync(t => t.UserId == userId);
|
||||||
@@ -133,7 +134,7 @@ namespace Notesnook.API.Controllers
|
|||||||
var userSetting = await userSettingsRepository.FindOneAsync(u => u.UserId == userId);
|
var userSetting = await userSettingsRepository.FindOneAsync(u => u.UserId == userId);
|
||||||
if (string.IsNullOrWhiteSpace(userSetting?.InboxKeys?.Public))
|
if (string.IsNullOrWhiteSpace(userSetting?.InboxKeys?.Public))
|
||||||
{
|
{
|
||||||
return BadRequest(new { error = "Inbox public key is not configured." });
|
return NotFound(new { error = "Inbox public key is not configured." });
|
||||||
}
|
}
|
||||||
return Ok(new { key = userSetting.InboxKeys.Public });
|
return Ok(new { key = userSetting.InboxKeys.Public });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,8 +183,8 @@ namespace Notesnook.API.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userId = this.User.GetUserId();
|
var userId = this.User.GetUserId();
|
||||||
var size = await s3Service.GetObjectSizeAsync(userId, name);
|
var size = await s3Service.GetObjectSizeAsync(userId, name); Response.Headers.ContentLength = size;
|
||||||
HttpContext.Response.Headers.ContentLength = size;
|
Response.Headers["X-Object-Size"] = size.ToString();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -93,38 +93,39 @@ namespace Notesnook.API.Controllers
|
|||||||
[HttpPatch("password/{type}")]
|
[HttpPatch("password/{type}")]
|
||||||
public async Task<IActionResult> ChangePassword([FromRoute] string type, [FromBody] ChangePasswordForm form)
|
public async Task<IActionResult> ChangePassword([FromRoute] string type, [FromBody] ChangePasswordForm form)
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
return BadRequest(new { error = "Password change is currently disabled." });
|
||||||
var clientId = User.FindFirstValue("client_id");
|
// var userId = User.GetUserId();
|
||||||
var jti = User.FindFirstValue("jti");
|
// var clientId = User.FindFirstValue("client_id");
|
||||||
var isPasswordReset = type == "reset";
|
// var jti = User.FindFirstValue("jti");
|
||||||
try
|
// var isPasswordReset = type == "reset";
|
||||||
{
|
// try
|
||||||
var result = isPasswordReset ? await serviceAccessor.UserAccountService.ResetPasswordAsync(userId, form.NewPassword) : await serviceAccessor.UserAccountService.ChangePasswordAsync(userId, form.OldPassword, form.NewPassword);
|
// {
|
||||||
if (!result)
|
// var result = isPasswordReset ? await serviceAccessor.UserAccountService.ResetPasswordAsync(userId, form.NewPassword) : await serviceAccessor.UserAccountService.ChangePasswordAsync(userId, form.OldPassword, form.NewPassword);
|
||||||
return BadRequest("Failed to change password.");
|
// if (!result)
|
||||||
|
// return BadRequest("Failed to change password.");
|
||||||
|
|
||||||
await UserService.SetUserKeysAsync(userId, form.UserKeys);
|
// await UserService.SetUserKeysAsync(userId, form.UserKeys);
|
||||||
|
|
||||||
await serviceAccessor.UserAccountService.ClearSessionsAsync(userId, clientId, all: false, jti, null);
|
// await serviceAccessor.UserAccountService.ClearSessionsAsync(userId, clientId, all: false, jti, null);
|
||||||
|
|
||||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
// await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
||||||
{
|
// {
|
||||||
UserId = userId,
|
// UserId = userId,
|
||||||
OriginTokenId = jti,
|
// OriginTokenId = jti,
|
||||||
Message = new Message
|
// Message = new Message
|
||||||
{
|
// {
|
||||||
Type = "logout",
|
// Type = "logout",
|
||||||
Data = JsonSerializer.Serialize(new { reason = "Password changed." })
|
// Data = JsonSerializer.Serialize(new { reason = "Password changed." })
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return Ok();
|
// return Ok();
|
||||||
}
|
// }
|
||||||
catch (Exception ex)
|
// catch (Exception ex)
|
||||||
{
|
// {
|
||||||
logger.LogError(ex, "Failed to change password");
|
// logger.LogError(ex, "Failed to change password");
|
||||||
return BadRequest(new { error = ex.Message });
|
// return BadRequest(new { error = ex.Message });
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("reset")]
|
[HttpPost("reset")]
|
||||||
|
|||||||
@@ -57,21 +57,9 @@ namespace Notesnook.API.Hubs
|
|||||||
private ISyncItemsRepositoryAccessor Repositories { get; }
|
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||||
private SyncDeviceService SyncDeviceService { get; }
|
private SyncDeviceService SyncDeviceService { get; }
|
||||||
private readonly IUnitOfWork unit;
|
private readonly IUnitOfWork unit;
|
||||||
private static readonly string[] CollectionKeys = [
|
|
||||||
"settingitem",
|
|
||||||
"attachment",
|
|
||||||
"note",
|
|
||||||
"notebook",
|
|
||||||
"content",
|
|
||||||
"shortcut",
|
|
||||||
"reminder",
|
|
||||||
"color",
|
|
||||||
"tag",
|
|
||||||
"vault",
|
|
||||||
"relation", // relations must sync at the end to prevent invalid state
|
|
||||||
];
|
|
||||||
private readonly FrozenDictionary<string, Action<IEnumerable<SyncItem>, string, long>> UpsertActionsMap;
|
private readonly FrozenDictionary<string, Action<IEnumerable<SyncItem>, string, long>> UpsertActionsMap;
|
||||||
private readonly Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>>[] Collections;
|
private readonly CollectionDef[] BaseCollectionDefs;
|
||||||
|
private readonly CollectionDef[] V4CollectionDefs;
|
||||||
ILogger<SyncV2Hub> Logger { get; }
|
ILogger<SyncV2Hub> Logger { get; }
|
||||||
|
|
||||||
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork, SyncDeviceService syncDeviceService, ILogger<SyncV2Hub> logger)
|
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork, SyncDeviceService syncDeviceService, ILogger<SyncV2Hub> logger)
|
||||||
@@ -81,18 +69,32 @@ namespace Notesnook.API.Hubs
|
|||||||
unit = unitOfWork;
|
unit = unitOfWork;
|
||||||
SyncDeviceService = syncDeviceService;
|
SyncDeviceService = syncDeviceService;
|
||||||
|
|
||||||
Collections = [
|
BaseCollectionDefs = [
|
||||||
Repositories.Settings.FindItemsById,
|
new("settingitem", Repositories.Settings.FindItemsById),
|
||||||
Repositories.Attachments.FindItemsById,
|
new("attachment", Repositories.Attachments.FindItemsById),
|
||||||
Repositories.Notes.FindItemsById,
|
new("note", Repositories.Notes.FindItemsById),
|
||||||
Repositories.Notebooks.FindItemsById,
|
new("notebook", Repositories.Notebooks.FindItemsById),
|
||||||
Repositories.Contents.FindItemsById,
|
new("content", Repositories.Contents.FindItemsById),
|
||||||
Repositories.Shortcuts.FindItemsById,
|
new("shortcut", Repositories.Shortcuts.FindItemsById),
|
||||||
Repositories.Reminders.FindItemsById,
|
new("reminder", Repositories.Reminders.FindItemsById),
|
||||||
Repositories.Colors.FindItemsById,
|
new("color", Repositories.Colors.FindItemsById),
|
||||||
Repositories.Tags.FindItemsById,
|
new("tag", Repositories.Tags.FindItemsById),
|
||||||
Repositories.Vaults.FindItemsById,
|
new("vault", Repositories.Vaults.FindItemsById),
|
||||||
Repositories.Relations.FindItemsById,
|
new("relation", Repositories.Relations.FindItemsById), // relations must sync at the end to prevent invalid state
|
||||||
|
];
|
||||||
|
V4CollectionDefs = [
|
||||||
|
new("settingitem", Repositories.Settings.FindItemsById),
|
||||||
|
new("attachment", Repositories.Attachments.FindItemsById),
|
||||||
|
new("note", Repositories.Notes.FindItemsById),
|
||||||
|
new("notebook", Repositories.Notebooks.FindItemsById),
|
||||||
|
new("content", Repositories.Contents.FindItemsById),
|
||||||
|
new("shortcut", Repositories.Shortcuts.FindItemsById),
|
||||||
|
new("reminder", Repositories.Reminders.FindItemsById),
|
||||||
|
new("color", Repositories.Colors.FindItemsById),
|
||||||
|
new("tag", Repositories.Tags.FindItemsById),
|
||||||
|
new("vault", Repositories.Vaults.FindItemsById),
|
||||||
|
new("inboxitemhistory", Repositories.InboxItemsHistory.FindItemsById),
|
||||||
|
new("relation", Repositories.Relations.FindItemsById), // relations must sync at the end to prevent invalid state
|
||||||
];
|
];
|
||||||
UpsertActionsMap = new Dictionary<string, Action<IEnumerable<SyncItem>, string, long>> {
|
UpsertActionsMap = new Dictionary<string, Action<IEnumerable<SyncItem>, string, long>> {
|
||||||
{ "settingitem", Repositories.Settings.UpsertMany },
|
{ "settingitem", Repositories.Settings.UpsertMany },
|
||||||
@@ -106,6 +108,7 @@ namespace Notesnook.API.Hubs
|
|||||||
{ "color", Repositories.Colors.UpsertMany },
|
{ "color", Repositories.Colors.UpsertMany },
|
||||||
{ "vault", Repositories.Vaults.UpsertMany },
|
{ "vault", Repositories.Vaults.UpsertMany },
|
||||||
{ "tag", Repositories.Tags.UpsertMany },
|
{ "tag", Repositories.Tags.UpsertMany },
|
||||||
|
{ "inboxitemhistory", Repositories.InboxItemsHistory.UpsertMany },
|
||||||
}.ToFrozenDictionary();
|
}.ToFrozenDictionary();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +125,19 @@ namespace Notesnook.API.Hubs
|
|||||||
await base.OnConnectedAsync();
|
await base.OnConnectedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
|
{
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(exception, "Connection {ConnectionId} disconnected with error (server-side drop)", Context.ConnectionId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Connection {ConnectionId} disconnected cleanly (client-initiated)", Context.ConnectionId);
|
||||||
|
}
|
||||||
|
await base.OnDisconnectedAsync(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<int> PushItems(string deviceId, SyncTransferItemV2 pushItem)
|
public async Task<int> PushItems(string deviceId, SyncTransferItemV2 pushItem)
|
||||||
{
|
{
|
||||||
@@ -168,17 +184,17 @@ namespace Notesnook.API.Hubs
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, HashSet<ItemKey> ids, int size, bool resetSync, long maxBytes)
|
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, HashSet<ItemKey> ids, int size, bool resetSync, long maxBytes, CollectionDef[] collectionDefs)
|
||||||
{
|
{
|
||||||
var itemsProcessed = 0;
|
var itemsProcessed = 0;
|
||||||
for (int i = 0; i < Collections.Length; i++)
|
foreach (var def in collectionDefs)
|
||||||
{
|
{
|
||||||
var type = CollectionKeys[i];
|
var type = def.Key;
|
||||||
|
|
||||||
var filteredIds = ids.Where((id) => id.Type == type).Select((id) => id.ItemId).ToArray();
|
var filteredIds = ids.Where((id) => id.Type == type).Select((id) => id.ItemId).ToArray();
|
||||||
if (!resetSync && filteredIds.Length == 0) continue;
|
if (!resetSync && filteredIds.Length == 0) continue;
|
||||||
|
|
||||||
using var cursor = await Collections[i](userId, filteredIds, resetSync, size);
|
using var cursor = await def.FindItems(userId, filteredIds, resetSync, size);
|
||||||
|
|
||||||
var chunk = new List<SyncItem>();
|
var chunk = new List<SyncItem>();
|
||||||
long totalBytes = 0;
|
long totalBytes = 0;
|
||||||
@@ -220,20 +236,25 @@ namespace Notesnook.API.Hubs
|
|||||||
|
|
||||||
public async Task<SyncV2Metadata> RequestFetch(string deviceId)
|
public async Task<SyncV2Metadata> RequestFetch(string deviceId)
|
||||||
{
|
{
|
||||||
return await HandleRequestFetch(deviceId, false, false);
|
return await HandleRequestFetch(deviceId, false, false, BaseCollectionDefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SyncV2Metadata> RequestFetchV2(string deviceId)
|
public async Task<SyncV2Metadata> RequestFetchV2(string deviceId)
|
||||||
{
|
{
|
||||||
return await HandleRequestFetch(deviceId, true, false);
|
return await HandleRequestFetch(deviceId, true, false, BaseCollectionDefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SyncV2Metadata> RequestFetchV3(string deviceId)
|
public async Task<SyncV2Metadata> RequestFetchV3(string deviceId)
|
||||||
{
|
{
|
||||||
return await HandleRequestFetch(deviceId, true, true);
|
return await HandleRequestFetch(deviceId, true, true, BaseCollectionDefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SyncV2Metadata> HandleRequestFetch(string deviceId, bool includeMonographs, bool includeInboxItems)
|
public async Task<SyncV2Metadata> RequestFetchV4(string deviceId)
|
||||||
|
{
|
||||||
|
return await HandleRequestFetch(deviceId, true, true, V4CollectionDefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<SyncV2Metadata> HandleRequestFetch(string deviceId, bool includeMonographs, bool includeInboxItems, CollectionDef[] collectionDefs)
|
||||||
{
|
{
|
||||||
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync.");
|
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync.");
|
||||||
|
|
||||||
@@ -257,7 +278,8 @@ namespace Notesnook.API.Hubs
|
|||||||
ids,
|
ids,
|
||||||
size: 100,
|
size: 100,
|
||||||
resetSync: device.IsSyncReset,
|
resetSync: device.IsSyncReset,
|
||||||
maxBytes: 3 * 1024 * 1024
|
maxBytes: 3 * 1024 * 1024,
|
||||||
|
collectionDefs
|
||||||
);
|
);
|
||||||
|
|
||||||
await foreach (var chunk in chunks)
|
await foreach (var chunk in chunks)
|
||||||
@@ -312,7 +334,7 @@ namespace Notesnook.API.Hubs
|
|||||||
var unsyncedInboxItemIds = ids.Where(k => k.Type == "inbox_item").Select(k => k.ItemId);
|
var unsyncedInboxItemIds = ids.Where(k => k.Type == "inbox_item").Select(k => k.ItemId);
|
||||||
var userInboxItems = device.IsSyncReset
|
var userInboxItems = device.IsSyncReset
|
||||||
? await Repositories.InboxItems.FindAsync(m => m.UserId == userId)
|
? await Repositories.InboxItems.FindAsync(m => m.UserId == userId)
|
||||||
: await Repositories.InboxItems.FindAsync(m => m.UserId == userId && unsyncedInboxItemIds.Contains(m.ItemId ?? m.Id.ToString()));
|
: await Repositories.InboxItems.FindAsync(m => m.UserId == userId && unsyncedInboxItemIds.Contains(m.ItemId));
|
||||||
if (userInboxItems.Any() && !await Clients.Caller.SendInboxItems(userInboxItems).WaitAsync(TimeSpan.FromMinutes(10)))
|
if (userInboxItems.Any() && !await Clients.Caller.SendInboxItems(userInboxItems).WaitAsync(TimeSpan.FromMinutes(10)))
|
||||||
{
|
{
|
||||||
throw new HubException("Client rejected inbox items.");
|
throw new HubException("Client rejected inbox items.");
|
||||||
@@ -331,6 +353,11 @@ namespace Notesnook.API.Hubs
|
|||||||
SyncEventCounterSource.Log.RecordFetchDuration(stopwatch.ElapsedMilliseconds);
|
SyncEventCounterSource.Log.RecordFetchDuration(stopwatch.ElapsedMilliseconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record CollectionDef(
|
||||||
|
string Key,
|
||||||
|
Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>> FindItems
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessagePack.MessagePackObject]
|
[MessagePack.MessagePackObject]
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ namespace Notesnook.API.Interfaces
|
|||||||
SyncItemsRepository Colors { get; }
|
SyncItemsRepository Colors { get; }
|
||||||
SyncItemsRepository Vaults { get; }
|
SyncItemsRepository Vaults { get; }
|
||||||
SyncItemsRepository Tags { get; }
|
SyncItemsRepository Tags { get; }
|
||||||
|
SyncItemsRepository InboxItemsHistory { get; }
|
||||||
Repository<UserSettings> UsersSettings { get; }
|
Repository<UserSettings> UsersSettings { get; }
|
||||||
Repository<Monograph> Monographs { get; }
|
Repository<Monograph> Monographs { get; }
|
||||||
Repository<InboxApiKey> InboxApiKey { get; }
|
Repository<InboxApiKey> InboxApiKey { get; }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="1.3.0" />
|
<PackageReference Include="AngleSharp" Version="1.3.0" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Aws.S3" Version="9.0.0" />
|
<PackageReference Include="AspNetCore.HealthChecks.Aws.S3" Version="9.0.0" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="9.0.0" />
|
||||||
<PackageReference Include="AWSSDK.Core" Version="3.7.304.31" />
|
<PackageReference Include="AWSSDK.Core" Version="3.7.304.31" />
|
||||||
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
||||||
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="6.2.0" />
|
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="6.2.0" />
|
||||||
|
|||||||
@@ -174,18 +174,19 @@ namespace Notesnook.API.Services
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
userSettings.InboxKeys = keys.InboxKeys;
|
userSettings.InboxKeys = keys.InboxKeys;
|
||||||
var defaultInboxKey = new InboxApiKey
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
Name = "Default",
|
|
||||||
DateCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
||||||
ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds(),
|
|
||||||
LastUsedAt = 0
|
|
||||||
};
|
|
||||||
await Repositories.InboxApiKey.InsertAsync(defaultInboxKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Repositories.InboxItems.DeleteManyAsync(t => t.UserId == userId);
|
await Repositories.InboxItems.DeleteManyAsync(t => t.UserId == userId);
|
||||||
|
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
||||||
|
{
|
||||||
|
OriginTokenId = null,
|
||||||
|
UserId = userId,
|
||||||
|
Message = new Message
|
||||||
|
{
|
||||||
|
Type = "inboxUpdated",
|
||||||
|
Data = JsonSerializer.Serialize(new { reason = "Inbox PGP keys added, updated, or removed." })
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
|
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
|
||||||
@@ -208,6 +209,7 @@ namespace Notesnook.API.Services
|
|||||||
Repositories.Colors.DeleteByUserId(userId);
|
Repositories.Colors.DeleteByUserId(userId);
|
||||||
Repositories.Tags.DeleteByUserId(userId);
|
Repositories.Tags.DeleteByUserId(userId);
|
||||||
Repositories.Vaults.DeleteByUserId(userId);
|
Repositories.Vaults.DeleteByUserId(userId);
|
||||||
|
Repositories.InboxItemsHistory.DeleteByUserId(userId);
|
||||||
Repositories.UsersSettings.Delete((u) => u.UserId == userId);
|
Repositories.UsersSettings.Delete((u) => u.UserId == userId);
|
||||||
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
|
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
|
||||||
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
|
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
|
||||||
@@ -269,6 +271,7 @@ namespace Notesnook.API.Services
|
|||||||
Repositories.Colors.DeleteByUserId(userId);
|
Repositories.Colors.DeleteByUserId(userId);
|
||||||
Repositories.Tags.DeleteByUserId(userId);
|
Repositories.Tags.DeleteByUserId(userId);
|
||||||
Repositories.Vaults.DeleteByUserId(userId);
|
Repositories.Vaults.DeleteByUserId(userId);
|
||||||
|
Repositories.InboxItemsHistory.DeleteByUserId(userId);
|
||||||
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
|
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
|
||||||
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
|
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
|
||||||
if (!await unit.Commit()) return false;
|
if (!await unit.Commit()) return false;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ using System.Text;
|
|||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Amazon.Runtime;
|
using Amazon.Runtime;
|
||||||
|
using StackExchange.Redis;
|
||||||
using IdentityModel.AspNetCore.OAuth2Introspection;
|
using IdentityModel.AspNetCore.OAuth2Introspection;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
@@ -196,7 +197,8 @@ namespace Notesnook.API
|
|||||||
.AddMongoCollection(Collections.ColorsKey)
|
.AddMongoCollection(Collections.ColorsKey)
|
||||||
.AddMongoCollection(Collections.VaultsKey)
|
.AddMongoCollection(Collections.VaultsKey)
|
||||||
.AddMongoCollection(Collections.InboxItemsKey)
|
.AddMongoCollection(Collections.InboxItemsKey)
|
||||||
.AddMongoCollection(Collections.InboxApiKeysKey);
|
.AddMongoCollection(Collections.InboxApiKeysKey)
|
||||||
|
.AddMongoCollection(Collections.InboxItemsHistoryKey);
|
||||||
|
|
||||||
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
|
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
|
||||||
services.AddScoped<SyncDeviceService>();
|
services.AddScoped<SyncDeviceService>();
|
||||||
@@ -219,7 +221,20 @@ namespace Notesnook.API
|
|||||||
}).AddMessagePackProtocol().AddJsonProtocol();
|
}).AddMessagePackProtocol().AddJsonProtocol();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Constants.SIGNALR_REDIS_CONNECTION_STRING))
|
if (!string.IsNullOrEmpty(Constants.SIGNALR_REDIS_CONNECTION_STRING))
|
||||||
signalR.AddStackExchangeRedis(Constants.SIGNALR_REDIS_CONNECTION_STRING);
|
{
|
||||||
|
services.AddHealthChecks()
|
||||||
|
.AddRedis(Constants.SIGNALR_REDIS_CONNECTION_STRING, tags: ["ready"]);
|
||||||
|
signalR.AddStackExchangeRedis(options =>
|
||||||
|
{
|
||||||
|
options.Configuration = ConfigurationOptions.Parse(Constants.SIGNALR_REDIS_CONNECTION_STRING);
|
||||||
|
options.Configuration.AbortOnConnectFail = false;
|
||||||
|
options.Configuration.ConnectRetry = 5;
|
||||||
|
options.Configuration.ReconnectRetryPolicy = new ExponentialRetry(5000, 30000);
|
||||||
|
options.Configuration.KeepAlive = 60;
|
||||||
|
options.Configuration.ConnectTimeout = 5000;
|
||||||
|
options.Configuration.SyncTimeout = 5000;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
services.AddResponseCompression(options =>
|
services.AddResponseCompression(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const RawInboxItemSchema = z.object({
|
|||||||
notebookIds: z.array(z.string()).optional(),
|
notebookIds: z.array(z.string()).optional(),
|
||||||
tagIds: z.array(z.string()).optional(),
|
tagIds: z.array(z.string()).optional(),
|
||||||
type: z.enum(["note"]),
|
type: z.enum(["note"]),
|
||||||
source: z.string(),
|
source: z.string().min(1, "Source is required"),
|
||||||
version: z.literal(1),
|
version: z.literal(1),
|
||||||
content: z
|
content: z
|
||||||
.object({
|
.object({
|
||||||
@@ -56,7 +56,9 @@ async function encrypt(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getInboxPublicEncryptionKey(apiKey: string) {
|
async function getInboxPublicEncryptionKey(
|
||||||
|
apiKey: string,
|
||||||
|
): Promise<{ status: "unauthorized" } | { status: "ok"; key: string | null }> {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${NOTESNOOK_API_SERVER_URL}/inbox/public-encryption-key`,
|
`${NOTESNOOK_API_SERVER_URL}/inbox/public-encryption-key`,
|
||||||
{
|
{
|
||||||
@@ -65,6 +67,9 @@ async function getInboxPublicEncryptionKey(apiKey: string) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if (response.status === 401) {
|
||||||
|
return { status: "unauthorized" };
|
||||||
|
}
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`failed to fetch inbox public encryption key: ${await response.text()}`,
|
`failed to fetch inbox public encryption key: ${await response.text()}`,
|
||||||
@@ -72,7 +77,7 @@ async function getInboxPublicEncryptionKey(apiKey: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as unknown as any;
|
const data = (await response.json()) as unknown as any;
|
||||||
return (data?.key as string) || null;
|
return { status: "ok", key: (data?.key as string) || null };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postEncryptedInboxItem(
|
async function postEncryptedInboxItem(
|
||||||
@@ -110,10 +115,14 @@ app.post("/", async (req, res) => {
|
|||||||
return res.status(401).json({ error: "unauthorized" });
|
return res.status(401).json({ error: "unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const inboxPublicKey = await getInboxPublicEncryptionKey(apiKey);
|
const encryptionKeyResult = await getInboxPublicEncryptionKey(apiKey);
|
||||||
if (!inboxPublicKey) {
|
if (encryptionKeyResult.status === "unauthorized") {
|
||||||
return res.status(403).json({ error: "inbox public key not found" });
|
return res.status(401).json({ error: "unauthorized" });
|
||||||
}
|
}
|
||||||
|
if (!encryptionKeyResult.key) {
|
||||||
|
return res.status(404).json({ error: "inbox public key not found" });
|
||||||
|
}
|
||||||
|
const inboxPublicKey = encryptionKeyResult.key;
|
||||||
console.log("[info] fetched inbox public key");
|
console.log("[info] fetched inbox public key");
|
||||||
|
|
||||||
const validationResult = RawInboxItemSchema.safeParse(req.body);
|
const validationResult = RawInboxItemSchema.safeParse(req.body);
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ namespace Streetwriters.Common.Extensions
|
|||||||
b.WithOrigins(Constants.NOTESNOOK_CORS_ORIGINS);
|
b.WithOrigins(Constants.NOTESNOOK_CORS_ORIGINS);
|
||||||
|
|
||||||
b.AllowAnyMethod()
|
b.AllowAnyMethod()
|
||||||
.AllowAnyHeader();
|
.AllowAnyHeader()
|
||||||
|
.WithExposedHeaders(["X-Object-Size", "Content-Length"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Mail;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MimeKit.Cryptography;
|
using MimeKit.Cryptography;
|
||||||
@@ -11,7 +12,7 @@ namespace Streetwriters.Common.Interfaces
|
|||||||
Task SendEmailAsync(
|
Task SendEmailAsync(
|
||||||
string email,
|
string email,
|
||||||
EmailTemplate template,
|
EmailTemplate template,
|
||||||
IClient client,
|
MailAddress from,
|
||||||
GnuPGContext? gpgContext = null,
|
GnuPGContext? gpgContext = null,
|
||||||
Dictionary<string, byte[]>? attachments = null
|
Dictionary<string, byte[]>? attachments = null
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
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,214 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,511 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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 required Refund Refund { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class Refund
|
|
||||||
{
|
|
||||||
[JsonPropertyName("refund_request_id")]
|
|
||||||
public long RefundRequestId { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -93,6 +93,9 @@ namespace Streetwriters.Common.Models
|
|||||||
[JsonPropertyName("trialsAvailed")]
|
[JsonPropertyName("trialsAvailed")]
|
||||||
public SubscriptionPlan[]? TrialsAvailed { get; set; }
|
public SubscriptionPlan[]? TrialsAvailed { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("extensionsAvailed")]
|
||||||
|
public SubscriptionExtension[]? ExtensionsAvailed { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("updatedAt")]
|
[JsonPropertyName("updatedAt")]
|
||||||
public long UpdatedAt { get; set; }
|
public long UpdatedAt { get; set; }
|
||||||
|
|
||||||
@@ -104,4 +107,16 @@ namespace Streetwriters.Common.Models
|
|||||||
[JsonPropertyName("status")]
|
[JsonPropertyName("status")]
|
||||||
public SubscriptionStatus Status { get; set; }
|
public SubscriptionStatus Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SubscriptionExtension
|
||||||
|
{
|
||||||
|
[JsonPropertyName("timestamp")]
|
||||||
|
public required long Timestamp { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("expiry")]
|
||||||
|
public required long ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public required string Type { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ namespace Streetwriters.Common.Services
|
|||||||
public async Task SendEmailAsync(
|
public async Task SendEmailAsync(
|
||||||
string email,
|
string email,
|
||||||
EmailTemplate template,
|
EmailTemplate template,
|
||||||
IClient client,
|
System.Net.Mail.MailAddress from,
|
||||||
GnuPGContext? gpgContext = null,
|
GnuPGContext? gpgContext = null,
|
||||||
Dictionary<string, byte[]>? attachments = null
|
Dictionary<string, byte[]>? attachments = null
|
||||||
)
|
)
|
||||||
@@ -55,8 +55,7 @@ namespace Streetwriters.Common.Services
|
|||||||
);
|
);
|
||||||
|
|
||||||
var message = new MimeMessage();
|
var message = new MimeMessage();
|
||||||
var sender = new MailboxAddress(client.SenderName, client.SenderEmail);
|
message.From.Add(new MailboxAddress(from.DisplayName, from.Address));
|
||||||
message.From.Add(sender);
|
|
||||||
message.To.Add(new MailboxAddress("", email));
|
message.To.Add(new MailboxAddress("", email));
|
||||||
message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data);
|
message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data);
|
||||||
|
|
||||||
@@ -65,8 +64,7 @@ namespace Streetwriters.Common.Services
|
|||||||
|
|
||||||
message.Body = await GetEmailBodyAsync(
|
message.Body = await GetEmailBodyAsync(
|
||||||
template,
|
template,
|
||||||
client,
|
new MailboxAddress(from.DisplayName, from.Address),
|
||||||
sender,
|
|
||||||
gpgContext,
|
gpgContext,
|
||||||
attachments
|
attachments
|
||||||
);
|
);
|
||||||
@@ -76,7 +74,6 @@ namespace Streetwriters.Common.Services
|
|||||||
|
|
||||||
private async Task<MimeEntity> GetEmailBodyAsync(
|
private async Task<MimeEntity> GetEmailBodyAsync(
|
||||||
EmailTemplate template,
|
EmailTemplate template,
|
||||||
IClient client,
|
|
||||||
MailboxAddress sender,
|
MailboxAddress sender,
|
||||||
GnuPGContext? gpgContext = null,
|
GnuPGContext? gpgContext = null,
|
||||||
Dictionary<string, byte[]>? attachments = null
|
Dictionary<string, byte[]>? attachments = null
|
||||||
@@ -107,7 +104,7 @@ namespace Streetwriters.Common.Services
|
|||||||
}
|
}
|
||||||
outputStream.Seek(0, SeekOrigin.Begin);
|
outputStream.Seek(0, SeekOrigin.Begin);
|
||||||
builder.Attachments.Add(
|
builder.Attachments.Add(
|
||||||
$"{client.Id}_pub.asc",
|
$"pub.asc",
|
||||||
Encoding.ASCII.GetBytes(
|
Encoding.ASCII.GetBytes(
|
||||||
Encoding.ASCII.GetString(outputStream.ToArray())
|
Encoding.ASCII.GetString(outputStream.ToArray())
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
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?.Transaction?.CustomerId == null) return null;
|
|
||||||
var url = $"{PADDLE_BASE_URI}/customers/{transaction.Transaction.CustomerId}";
|
|
||||||
var response = await httpClient.GetFromJsonAsync<GetCustomerResponse>(url);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
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 == null || 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 ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -190,8 +190,8 @@ namespace Streetwriters.Identity.Controllers
|
|||||||
var client = Clients.FindClientById(form.ClientId);
|
var client = Clients.FindClientById(form.ClientId);
|
||||||
if (client == null) return BadRequest("Invalid client_id.");
|
if (client == null) return BadRequest("Invalid client_id.");
|
||||||
|
|
||||||
var user = await UserManager.FindByEmailAsync(form.Email) ?? throw new Exception("User not found.");
|
var user = await UserManager.FindByEmailAsync(form.Email);
|
||||||
if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok();
|
if (user == null || !await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok();
|
||||||
|
|
||||||
var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword");
|
var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword");
|
||||||
var callbackUrl = UrlExtensions.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD);
|
var callbackUrl = UrlExtensions.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace Streetwriters.Identity.Interfaces
|
|||||||
{
|
{
|
||||||
public interface ISMSSender
|
public interface ISMSSender
|
||||||
{
|
{
|
||||||
Task<string> SendOTPAsync(string number, IClient client);
|
Task<string?> SendOTPAsync(string number, IClient client);
|
||||||
Task<bool> VerifyOTPAsync(string id, string code);
|
Task<bool> VerifyOTPAsync(string id, string code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,6 +186,8 @@ namespace Streetwriters.Identity.Services
|
|||||||
ArgumentNullException.ThrowIfNull(form.PhoneNumber);
|
ArgumentNullException.ThrowIfNull(form.PhoneNumber);
|
||||||
await UserManager.SetPhoneNumberAsync(user, form.PhoneNumber);
|
await UserManager.SetPhoneNumberAsync(user, form.PhoneNumber);
|
||||||
var id = await SMSSender.SendOTPAsync(form.PhoneNumber, client);
|
var id = await SMSSender.SendOTPAsync(form.PhoneNumber, client);
|
||||||
|
if (string.IsNullOrEmpty(id)) throw new Exception("Failed to send SMS. Please try again.");
|
||||||
|
|
||||||
logger.LogInformation("SMS OTP sent for user: {UserId}, SMS ID: {SmsId}", user.Id, id);
|
logger.LogInformation("SMS OTP sent for user: {UserId}, SMS ID: {SmsId}", user.Id, id);
|
||||||
await this.ReplaceClaimAsync(user, MFAService.SMS_ID_CLAIM, id);
|
await this.ReplaceClaimAsync(user, MFAService.SMS_ID_CLAIM, id);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -23,36 +23,56 @@ using Streetwriters.Common;
|
|||||||
using Twilio.Rest.Verify.V2.Service;
|
using Twilio.Rest.Verify.V2.Service;
|
||||||
using Twilio;
|
using Twilio;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Streetwriters.Identity.Services
|
namespace Streetwriters.Identity.Services
|
||||||
{
|
{
|
||||||
public class SMSSender : ISMSSender
|
public class SMSSender : ISMSSender
|
||||||
{
|
{
|
||||||
public SMSSender()
|
private readonly ILogger<SMSSender> Logger;
|
||||||
|
public SMSSender(ILogger<SMSSender> logger)
|
||||||
{
|
{
|
||||||
|
Logger = logger;
|
||||||
if (!string.IsNullOrEmpty(Constants.TWILIO_ACCOUNT_SID) && !string.IsNullOrEmpty(Constants.TWILIO_AUTH_TOKEN))
|
if (!string.IsNullOrEmpty(Constants.TWILIO_ACCOUNT_SID) && !string.IsNullOrEmpty(Constants.TWILIO_AUTH_TOKEN))
|
||||||
{
|
{
|
||||||
TwilioClient.Init(Constants.TWILIO_ACCOUNT_SID, Constants.TWILIO_AUTH_TOKEN);
|
TwilioClient.Init(Constants.TWILIO_ACCOUNT_SID, Constants.TWILIO_AUTH_TOKEN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> SendOTPAsync(string number, IClient app)
|
public async Task<string?> SendOTPAsync(string number, IClient app)
|
||||||
{
|
{
|
||||||
var verification = await VerificationResource.CreateAsync(
|
try
|
||||||
to: number,
|
{
|
||||||
channel: "sms",
|
var verification = await VerificationResource.CreateAsync(
|
||||||
pathServiceSid: Constants.TWILIO_SERVICE_SID
|
to: number,
|
||||||
);
|
channel: "sms",
|
||||||
return verification.Sid;
|
pathServiceSid: Constants.TWILIO_SERVICE_SID
|
||||||
|
);
|
||||||
|
return verification.Sid;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error sending OTP with Twilio");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> VerifyOTPAsync(string id, string code)
|
public async Task<bool> VerifyOTPAsync(string id, string code)
|
||||||
{
|
{
|
||||||
return (await VerificationCheckResource.CreateAsync(
|
try
|
||||||
verificationSid: id,
|
{
|
||||||
pathServiceSid: Constants.TWILIO_SERVICE_SID,
|
return (await VerificationCheckResource.CreateAsync(
|
||||||
code: code
|
verificationSid: id,
|
||||||
)).Status == "approved";
|
pathServiceSid: Constants.TWILIO_SERVICE_SID,
|
||||||
|
code: code
|
||||||
|
)).Status == "approved";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error verifying OTP with Twilio");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ namespace Streetwriters.Identity.Services
|
|||||||
Subject = Email2FATemplate.Subject,
|
Subject = Email2FATemplate.Subject,
|
||||||
Data = new { app_name = client.Name, code },
|
Data = new { app_name = client.Name, code },
|
||||||
};
|
};
|
||||||
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
|
await EmailSender.SendEmailAsync(email, template, new System.Net.Mail.MailAddress(client.SenderEmail, client.SenderName), NNGnuPGContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendConfirmationEmailAsync(
|
public async Task SendConfirmationEmailAsync(
|
||||||
@@ -120,7 +120,7 @@ namespace Streetwriters.Identity.Services
|
|||||||
Subject = ConfirmEmailTemplate.Subject,
|
Subject = ConfirmEmailTemplate.Subject,
|
||||||
Data = new { app_name = client.Name, confirm_link = callbackUrl },
|
Data = new { app_name = client.Name, confirm_link = callbackUrl },
|
||||||
};
|
};
|
||||||
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
|
await EmailSender.SendEmailAsync(email, template, new System.Net.Mail.MailAddress(client.SenderEmail, client.SenderName), NNGnuPGContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendChangeEmailConfirmationAsync(
|
public async Task SendChangeEmailConfirmationAsync(
|
||||||
@@ -136,7 +136,7 @@ namespace Streetwriters.Identity.Services
|
|||||||
Subject = ConfirmChangeEmailTemplate.Subject,
|
Subject = ConfirmChangeEmailTemplate.Subject,
|
||||||
Data = new { app_name = client.Name, code },
|
Data = new { app_name = client.Name, code },
|
||||||
};
|
};
|
||||||
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
|
await EmailSender.SendEmailAsync(email, template, new System.Net.Mail.MailAddress(client.SenderEmail, client.SenderName), NNGnuPGContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendPasswordResetEmailAsync(
|
public async Task SendPasswordResetEmailAsync(
|
||||||
@@ -152,7 +152,7 @@ namespace Streetwriters.Identity.Services
|
|||||||
Subject = PasswordResetEmailTemplate.Subject,
|
Subject = PasswordResetEmailTemplate.Subject,
|
||||||
Data = new { app_name = client.Name, reset_link = callbackUrl },
|
Data = new { app_name = client.Name, reset_link = callbackUrl },
|
||||||
};
|
};
|
||||||
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
|
await EmailSender.SendEmailAsync(email, template, new System.Net.Mail.MailAddress(client.SenderEmail, client.SenderName), NNGnuPGContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client)
|
public async Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client)
|
||||||
@@ -168,7 +168,7 @@ namespace Streetwriters.Identity.Services
|
|||||||
device_info = deviceInfo.Replace("\n", "<br>"),
|
device_info = deviceInfo.Replace("\n", "<br>"),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
|
await EmailSender.SendEmailAsync(email, template, new System.Net.Mail.MailAddress(client.SenderEmail, client.SenderName), NNGnuPGContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,13 @@ namespace Streetwriters.Identity.Services
|
|||||||
|
|
||||||
await mfaService.ResetMFAAsync(user);
|
await mfaService.ResetMFAAsync(user);
|
||||||
result = await userManager.AddPasswordAsync(user, newPassword);
|
result = await userManager.AddPasswordAsync(user, newPassword);
|
||||||
|
|
||||||
|
// force change email to lowercase if it is not already
|
||||||
|
if (user.Email != null && user.Email != user.Email.ToLower())
|
||||||
|
{
|
||||||
|
var token = await userManager.GenerateChangeEmailTokenAsync(user, user.Email.ToLower());
|
||||||
|
result = await userManager.ChangeEmailAsync(user, user.Email.ToLower(), token);
|
||||||
|
}
|
||||||
return result.Succeeded;
|
return result.Succeeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -504,7 +504,6 @@
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
text-indent: 0px;
|
text-indent: 0px;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
white-space: pre-wrap;
|
|
||||||
widows: 2;
|
widows: 2;
|
||||||
word-spacing: 0px;
|
word-spacing: 0px;
|
||||||
-webkit-text-stroke-width: 0px;
|
-webkit-text-stroke-width: 0px;
|
||||||
@@ -536,7 +535,6 @@
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
text-indent: 0px;
|
text-indent: 0px;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
white-space: pre-wrap;
|
|
||||||
widows: 2;
|
widows: 2;
|
||||||
word-spacing: 0px;
|
word-spacing: 0px;
|
||||||
-webkit-text-stroke-width: 0px;
|
-webkit-text-stroke-width: 0px;
|
||||||
@@ -567,7 +565,6 @@
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
text-indent: 0px;
|
text-indent: 0px;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
white-space: pre-wrap;
|
|
||||||
widows: 2;
|
widows: 2;
|
||||||
word-spacing: 0px;
|
word-spacing: 0px;
|
||||||
-webkit-text-stroke-width: 0px;
|
-webkit-text-stroke-width: 0px;
|
||||||
|
|||||||
@@ -309,7 +309,6 @@
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
text-indent: 0px;
|
text-indent: 0px;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
white-space: pre-wrap;
|
|
||||||
widows: 2;
|
widows: 2;
|
||||||
word-spacing: 0px;
|
word-spacing: 0px;
|
||||||
-webkit-text-stroke-width: 0px;
|
-webkit-text-stroke-width: 0px;
|
||||||
|
|||||||
@@ -401,7 +401,6 @@
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
text-indent: 0px;
|
text-indent: 0px;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
white-space: pre-wrap;
|
|
||||||
word-spacing: 0px;
|
word-spacing: 0px;
|
||||||
-webkit-text-stroke-width: 0px;
|
-webkit-text-stroke-width: 0px;
|
||||||
background-color: rgb(
|
background-color: rgb(
|
||||||
|
|||||||
@@ -467,7 +467,6 @@
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
text-indent: 0px;
|
text-indent: 0px;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
white-space: pre-wrap;
|
|
||||||
widows: 2;
|
widows: 2;
|
||||||
word-spacing: 0px;
|
word-spacing: 0px;
|
||||||
-webkit-text-stroke-width: 0px;
|
-webkit-text-stroke-width: 0px;
|
||||||
@@ -483,9 +482,7 @@
|
|||||||
display: inline;
|
display: inline;
|
||||||
"
|
"
|
||||||
><em
|
><em
|
||||||
>If you did not request to reset
|
>If you did not request to reset your account password, you can safely ignore this email.</em
|
||||||
your account password, you can
|
|
||||||
safely ignore this email.</em
|
|
||||||
></span
|
></span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,7 +551,6 @@
|
|||||||
>
|
>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
.
|
|
||||||
<td
|
<td
|
||||||
style="
|
style="
|
||||||
padding: 18px 0px 18px 0px;
|
padding: 18px 0px 18px 0px;
|
||||||
|
|||||||
@@ -18,28 +18,49 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Lib.AspNetCore.ServerSentEvents;
|
using Lib.AspNetCore.ServerSentEvents;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Streetwriters.Messenger.Helpers
|
namespace Streetwriters.Messenger.Helpers
|
||||||
{
|
{
|
||||||
public class SSEHelper
|
public class SSEHelper
|
||||||
{
|
{
|
||||||
public static async Task SendEventToUserAsync(string data, IServerSentEventsService sseService, string userId, string? originTokenId = null)
|
public static async Task SendEventToUserAsync(string data, IServerSentEventsService sseService, string userId, string? originTokenId = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var clients = sseService.GetClients().Where(c => c.User.FindFirstValue("sub") == userId);
|
var clients = sseService.GetClients()
|
||||||
foreach (var client in clients)
|
.Where(c => c.User?.FindFirstValue("sub") == userId)
|
||||||
{
|
.Where(c => originTokenId == null || c.User?.FindFirstValue("jti") != originTokenId);
|
||||||
if (originTokenId != null && client.User.FindFirstValue("jti") == originTokenId) continue;
|
|
||||||
if (!client.IsConnected) continue;
|
await SendEventToClientsAsync(clients, data, cancellationToken);
|
||||||
await client.SendEventAsync(data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task SendEventToAllUsersAsync(string data, IServerSentEventsService sseService)
|
public static async Task SendEventToAllUsersAsync(string data, IServerSentEventsService sseService, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await sseService.SendEventAsync(data);
|
await SendEventToClientsAsync(sseService.GetClients(), data, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SendEventToClientsAsync(IEnumerable<IServerSentEventsClient> clients, string data, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (var client in clients)
|
||||||
|
{
|
||||||
|
if (!client.IsConnected) continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await client.SendEventAsync(data, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ using System;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Lib.AspNetCore.ServerSentEvents;
|
using Lib.AspNetCore.ServerSentEvents;
|
||||||
using Streetwriters.Messenger.Helpers;
|
using Streetwriters.Messenger.Helpers;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -33,12 +34,14 @@ namespace Streetwriters.Messenger.Services
|
|||||||
private const string HEARTBEAT_MESSAGE_FORMAT = "Streetwriters Heartbeat ({0} UTC)";
|
private const string HEARTBEAT_MESSAGE_FORMAT = "Streetwriters Heartbeat ({0} UTC)";
|
||||||
|
|
||||||
private readonly IServerSentEventsService _serverSentEventsService;
|
private readonly IServerSentEventsService _serverSentEventsService;
|
||||||
|
private readonly ILogger<HeartbeatService> _logger;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Constructor
|
#region Constructor
|
||||||
public HeartbeatService(IServerSentEventsService serverSentEventsService)
|
public HeartbeatService(IServerSentEventsService serverSentEventsService, ILogger<HeartbeatService> logger)
|
||||||
{
|
{
|
||||||
_serverSentEventsService = serverSentEventsService;
|
_serverSentEventsService = serverSentEventsService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -47,15 +50,28 @@ namespace Streetwriters.Messenger.Services
|
|||||||
{
|
{
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var message = JsonSerializer.Serialize(new
|
try
|
||||||
{
|
{
|
||||||
type = "heartbeat",
|
var message = JsonSerializer.Serialize(new
|
||||||
data = JsonSerializer.Serialize(new
|
|
||||||
{
|
{
|
||||||
t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
type = "heartbeat",
|
||||||
})
|
data = JsonSerializer.Serialize(new
|
||||||
});
|
{
|
||||||
await SSEHelper.SendEventToAllUsersAsync(message, _serverSentEventsService);
|
t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
await SSEHelper.SendEventToAllUsersAsync(message, _serverSentEventsService, stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to send SSE heartbeat to one or more clients.");
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
||||||
<PackageReference Include="Lib.AspNetCore.ServerSentEvents" Version="6.0.0" />
|
<PackageReference Include="Lib.AspNetCore.ServerSentEvents" Version="9.1.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.0"
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.0"
|
||||||
NoWarn="NU1605" />
|
NoWarn="NU1605" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.0"
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user