Compare commits

...
Author SHA1 Message Date
Abdullah Atta 7ce7f02646 api: disable password changing due to a critical issue 2026-07-16 11:32:39 +05:00
Abdullah Atta a1dbd3f8b8 common: simplify email sender api 2026-07-08 09:05:58 +05:00
Abdullah Atta 99489b9b4c common: remove internal paddle related code 2026-07-07 09:20:24 +05:00
Abdullah Atta 1b953f756e identity: force change email casing to lower case on password reset 2026-06-29 10:01:15 +05:00
01zulfiandGitHub d27ab68735 inbox: trigger inboxUpdated SSE whenever pgp keys change (#104) 2026-06-10 08:17:06 +05:00
Abdullah Atta 294d885dbf common: expose x-object-size & content-length header via cors 2026-06-06 10:18:30 +05:00
Abdullah Atta bdd5017394 s3: add x-object-size header alongwith content-length header 2026-06-06 08:57:53 +05:00
01zulfiandGitHub 7ad70c63ee api: move inboxitemhistory collection sync in RequestFetchV4 (#101) 2026-05-19 13:39:52 +05:00
Abdullah Atta 0367ab6f80 sync: get rid of ._id fallback for inbox items 2026-05-15 08:58:21 +05:00
01zulfiandGitHub f3bfe0957b inbox: create InboxItemsHistory synced collection (#96) 2026-05-14 11:20:17 +05:00
01zulfiandGitHub 7f614f6954 inbox: remove 'Default' api key creation (#100) 2026-05-14 11:09:28 +05:00
Abdullah Atta 6663778e3e identity: fix email templates 2026-05-14 10:45:10 +05:00
14f0a3b37e inbox: improve http response status codes (#99)
* inbox: improve http response status codes
* inbox api: respond with 401 unauthorized for invalid inbox api key
* notesnook api (get public encryption key): respond with 404 if not found

* Update Notesnook.Inbox.API/src/index.ts

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-05-14 09:37:28 +05:00
01zulfiandGitHub 82a1152f9f inbox: fix expiry date validation check (#97) 2026-05-13 09:51:24 +05:00
01zulfiandGitHub 580524b855 inbox: require non-empty source in inbox item (#98) 2026-05-13 09:50:18 +05:00
33 changed files with 156 additions and 1483 deletions
@@ -42,6 +42,7 @@ namespace Notesnook.API.Accessors
public SyncItemsRepository Colors { get; }
public SyncItemsRepository Vaults { get; }
public SyncItemsRepository Tags { get; }
public SyncItemsRepository InboxItemsHistory { get; }
public Repository<UserSettings> UsersSettings { get; }
public Repository<Monograph> Monographs { get; }
public Repository<InboxApiKey> InboxApiKey { get; }
@@ -75,6 +76,8 @@ namespace Notesnook.API.Accessors
IMongoCollection<SyncItem> vaults,
[FromKeyedServices(Collections.TagsKey)]
IMongoCollection<SyncItem> tags,
[FromKeyedServices(Collections.InboxItemsHistoryKey)]
IMongoCollection<SyncItem> inboxItemsHistory,
Repository<UserSettings> usersSettings,
Repository<Monograph> monographs,
@@ -102,6 +105,7 @@ namespace Notesnook.API.Accessors
Colors = new SyncItemsRepository(dbContext, colors, logger);
Vaults = new SyncItemsRepository(dbContext, vaults, logger);
Tags = new SyncItemsRepository(dbContext, tags, logger);
InboxItemsHistory = new SyncItemsRepository(dbContext, inboxItemsHistory, logger);
}
}
}
+1
View File
@@ -18,5 +18,6 @@ namespace Notesnook.API
public const string InboxApiKeysKey = "inbox_api_keys";
public const string SyncDevicesKey = "sync_devices";
public const string DeviceIdsChunksKey = "device_ids_chunks";
public const string InboxItemsHistoryKey = "inbox_items_history";
}
}
+4 -3
View File
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
@@ -72,9 +73,9 @@ namespace Notesnook.API.Controllers
{
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);
@@ -133,7 +134,7 @@ namespace Notesnook.API.Controllers
var userSetting = await userSettingsRepository.FindOneAsync(u => u.UserId == userId);
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 });
}
+2 -2
View File
@@ -183,8 +183,8 @@ namespace Notesnook.API.Controllers
try
{
var userId = this.User.GetUserId();
var size = await s3Service.GetObjectSizeAsync(userId, name);
HttpContext.Response.Headers.ContentLength = size;
var size = await s3Service.GetObjectSizeAsync(userId, name); Response.Headers.ContentLength = size;
Response.Headers["X-Object-Size"] = size.ToString();
return Ok();
}
catch (Exception ex)
+29 -28
View File
@@ -93,38 +93,39 @@ namespace Notesnook.API.Controllers
[HttpPatch("password/{type}")]
public async Task<IActionResult> ChangePassword([FromRoute] string type, [FromBody] ChangePasswordForm form)
{
var userId = User.GetUserId();
var clientId = User.FindFirstValue("client_id");
var jti = User.FindFirstValue("jti");
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)
return BadRequest("Failed to change password.");
return BadRequest(new { error = "Password change is currently disabled." });
// var userId = User.GetUserId();
// var clientId = User.FindFirstValue("client_id");
// var jti = User.FindFirstValue("jti");
// 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)
// 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
{
UserId = userId,
OriginTokenId = jti,
Message = new Message
{
Type = "logout",
Data = JsonSerializer.Serialize(new { reason = "Password changed." })
}
});
// await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
// {
// UserId = userId,
// OriginTokenId = jti,
// Message = new Message
// {
// Type = "logout",
// Data = JsonSerializer.Serialize(new { reason = "Password changed." })
// }
// });
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to change password");
return BadRequest(new { error = ex.Message });
}
// return Ok();
// }
// catch (Exception ex)
// {
// logger.LogError(ex, "Failed to change password");
// return BadRequest(new { error = ex.Message });
// }
}
[HttpPost("reset")]
+50 -36
View File
@@ -57,21 +57,9 @@ namespace Notesnook.API.Hubs
private ISyncItemsRepositoryAccessor Repositories { get; }
private SyncDeviceService SyncDeviceService { get; }
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 Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>>[] Collections;
private readonly CollectionDef[] BaseCollectionDefs;
private readonly CollectionDef[] V4CollectionDefs;
ILogger<SyncV2Hub> Logger { get; }
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork, SyncDeviceService syncDeviceService, ILogger<SyncV2Hub> logger)
@@ -81,18 +69,32 @@ namespace Notesnook.API.Hubs
unit = unitOfWork;
SyncDeviceService = syncDeviceService;
Collections = [
Repositories.Settings.FindItemsById,
Repositories.Attachments.FindItemsById,
Repositories.Notes.FindItemsById,
Repositories.Notebooks.FindItemsById,
Repositories.Contents.FindItemsById,
Repositories.Shortcuts.FindItemsById,
Repositories.Reminders.FindItemsById,
Repositories.Colors.FindItemsById,
Repositories.Tags.FindItemsById,
Repositories.Vaults.FindItemsById,
Repositories.Relations.FindItemsById,
BaseCollectionDefs = [
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("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>> {
{ "settingitem", Repositories.Settings.UpsertMany },
@@ -106,6 +108,7 @@ namespace Notesnook.API.Hubs
{ "color", Repositories.Colors.UpsertMany },
{ "vault", Repositories.Vaults.UpsertMany },
{ "tag", Repositories.Tags.UpsertMany },
{ "inboxitemhistory", Repositories.InboxItemsHistory.UpsertMany },
}.ToFrozenDictionary();
}
@@ -181,17 +184,17 @@ namespace Notesnook.API.Hubs
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;
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();
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>();
long totalBytes = 0;
@@ -233,20 +236,25 @@ namespace Notesnook.API.Hubs
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)
{
return await HandleRequestFetch(deviceId, true, false);
return await HandleRequestFetch(deviceId, true, false, BaseCollectionDefs);
}
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.");
@@ -270,7 +278,8 @@ namespace Notesnook.API.Hubs
ids,
size: 100,
resetSync: device.IsSyncReset,
maxBytes: 3 * 1024 * 1024
maxBytes: 3 * 1024 * 1024,
collectionDefs
);
await foreach (var chunk in chunks)
@@ -325,7 +334,7 @@ namespace Notesnook.API.Hubs
var unsyncedInboxItemIds = ids.Where(k => k.Type == "inbox_item").Select(k => k.ItemId);
var userInboxItems = device.IsSyncReset
? 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)))
{
throw new HubException("Client rejected inbox items.");
@@ -344,6 +353,11 @@ namespace Notesnook.API.Hubs
SyncEventCounterSource.Log.RecordFetchDuration(stopwatch.ElapsedMilliseconds);
}
}
private record CollectionDef(
string Key,
Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>> FindItems
);
}
[MessagePack.MessagePackObject]
@@ -38,6 +38,7 @@ namespace Notesnook.API.Interfaces
SyncItemsRepository Colors { get; }
SyncItemsRepository Vaults { get; }
SyncItemsRepository Tags { get; }
SyncItemsRepository InboxItemsHistory { get; }
Repository<UserSettings> UsersSettings { get; }
Repository<Monograph> Monographs { get; }
Repository<InboxApiKey> InboxApiKey { get; }
+12 -9
View File
@@ -174,18 +174,19 @@ namespace Notesnook.API.Services
else
{
userSettings.InboxKeys = keys.InboxKeys;
var defaultInboxKey = new InboxApiKey
{
UserId = userId,
Name = "Default",
DateCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds(),
LastUsedAt = 0
};
await Repositories.InboxApiKey.InsertAsync(defaultInboxKey);
}
await Repositories.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);
@@ -208,6 +209,7 @@ namespace Notesnook.API.Services
Repositories.Colors.DeleteByUserId(userId);
Repositories.Tags.DeleteByUserId(userId);
Repositories.Vaults.DeleteByUserId(userId);
Repositories.InboxItemsHistory.DeleteByUserId(userId);
Repositories.UsersSettings.Delete((u) => u.UserId == userId);
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
@@ -269,6 +271,7 @@ namespace Notesnook.API.Services
Repositories.Colors.DeleteByUserId(userId);
Repositories.Tags.DeleteByUserId(userId);
Repositories.Vaults.DeleteByUserId(userId);
Repositories.InboxItemsHistory.DeleteByUserId(userId);
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
if (!await unit.Commit()) return false;
+2 -1
View File
@@ -197,7 +197,8 @@ namespace Notesnook.API
.AddMongoCollection(Collections.ColorsKey)
.AddMongoCollection(Collections.VaultsKey)
.AddMongoCollection(Collections.InboxItemsKey)
.AddMongoCollection(Collections.InboxApiKeysKey);
.AddMongoCollection(Collections.InboxApiKeysKey)
.AddMongoCollection(Collections.InboxItemsHistoryKey);
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
services.AddScoped<SyncDeviceService>();
+15 -6
View File
@@ -17,7 +17,7 @@ const RawInboxItemSchema = z.object({
notebookIds: z.array(z.string()).optional(),
tagIds: z.array(z.string()).optional(),
type: z.enum(["note"]),
source: z.string(),
source: z.string().min(1, "Source is required"),
version: z.literal(1),
content: z
.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(
`${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) {
throw new Error(
`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;
return (data?.key as string) || null;
return { status: "ok", key: (data?.key as string) || null };
}
async function postEncryptedInboxItem(
@@ -110,10 +115,14 @@ app.post("/", async (req, res) => {
return res.status(401).json({ error: "unauthorized" });
}
const inboxPublicKey = await getInboxPublicEncryptionKey(apiKey);
if (!inboxPublicKey) {
return res.status(403).json({ error: "inbox public key not found" });
const encryptionKeyResult = await getInboxPublicEncryptionKey(apiKey);
if (encryptionKeyResult.status === "unauthorized") {
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");
const validationResult = RawInboxItemSchema.safeParse(req.body);
@@ -52,7 +52,8 @@ namespace Streetwriters.Common.Extensions
b.WithOrigins(Constants.NOTESNOOK_CORS_ORIGINS);
b.AllowAnyMethod()
.AllowAnyHeader();
.AllowAnyHeader()
.WithExposedHeaders(["X-Object-Size", "Content-Length"]);
});
});
return services;
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Net.Mail;
using System.Threading.Tasks;
using MimeKit;
using MimeKit.Cryptography;
@@ -11,7 +12,7 @@ namespace Streetwriters.Common.Interfaces
Task SendEmailAsync(
string email,
EmailTemplate template,
IClient client,
MailAddress from,
GnuPGContext? gpgContext = 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")]
public SubscriptionPlan[]? TrialsAvailed { get; set; }
[JsonPropertyName("extensionsAvailed")]
public SubscriptionExtension[]? ExtensionsAvailed { get; set; }
[JsonPropertyName("updatedAt")]
public long UpdatedAt { get; set; }
@@ -104,4 +107,16 @@ namespace Streetwriters.Common.Models
[JsonPropertyName("status")]
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; }
}
}
+4 -7
View File
@@ -27,7 +27,7 @@ namespace Streetwriters.Common.Services
public async Task SendEmailAsync(
string email,
EmailTemplate template,
IClient client,
System.Net.Mail.MailAddress from,
GnuPGContext? gpgContext = null,
Dictionary<string, byte[]>? attachments = null
)
@@ -55,8 +55,7 @@ namespace Streetwriters.Common.Services
);
var message = new MimeMessage();
var sender = new MailboxAddress(client.SenderName, client.SenderEmail);
message.From.Add(sender);
message.From.Add(new MailboxAddress(from.DisplayName, from.Address));
message.To.Add(new MailboxAddress("", email));
message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data);
@@ -65,8 +64,7 @@ namespace Streetwriters.Common.Services
message.Body = await GetEmailBodyAsync(
template,
client,
sender,
new MailboxAddress(from.DisplayName, from.Address),
gpgContext,
attachments
);
@@ -76,7 +74,6 @@ namespace Streetwriters.Common.Services
private async Task<MimeEntity> GetEmailBodyAsync(
EmailTemplate template,
IClient client,
MailboxAddress sender,
GnuPGContext? gpgContext = null,
Dictionary<string, byte[]>? attachments = null
@@ -107,7 +104,7 @@ namespace Streetwriters.Common.Services
}
outputStream.Seek(0, SeekOrigin.Begin);
builder.Attachments.Add(
$"{client.Id}_pub.asc",
$"pub.asc",
Encoding.ASCII.GetBytes(
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;
}
}
}
@@ -104,7 +104,7 @@ namespace Streetwriters.Identity.Services
Subject = Email2FATemplate.Subject,
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(
@@ -120,7 +120,7 @@ namespace Streetwriters.Identity.Services
Subject = ConfirmEmailTemplate.Subject,
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(
@@ -136,7 +136,7 @@ namespace Streetwriters.Identity.Services
Subject = ConfirmChangeEmailTemplate.Subject,
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(
@@ -152,7 +152,7 @@ namespace Streetwriters.Identity.Services
Subject = PasswordResetEmailTemplate.Subject,
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)
@@ -168,7 +168,7 @@ namespace Streetwriters.Identity.Services
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);
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;
}
@@ -504,7 +504,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
@@ -536,7 +535,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
@@ -567,7 +565,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
@@ -309,7 +309,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
@@ -401,7 +401,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
@@ -467,7 +467,6 @@
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
@@ -483,9 +482,7 @@
display: inline;
"
><em
>If you did not request to reset
your account password, you can
safely ignore this email.</em
>If you did not request to reset your account password, you can safely ignore this email.</em
></span
>
</div>
@@ -554,7 +551,6 @@
>
<tbody>
<tr>
.
<td
style="
padding: 18px 0px 18px 0px;