forked from Kieran/void.cat
Add strike paywall (incomplete)
This commit is contained in:
parent
e098b2c0f0
commit
0eda25ba00
@ -1,6 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.Paywall;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Controllers;
|
namespace VoidCat.Controllers;
|
||||||
@ -9,12 +10,14 @@ namespace VoidCat.Controllers;
|
|||||||
public class DownloadController : Controller
|
public class DownloadController : Controller
|
||||||
{
|
{
|
||||||
private readonly IFileStore _storage;
|
private readonly IFileStore _storage;
|
||||||
|
private readonly IPaywallStore _paywall;
|
||||||
private readonly ILogger<DownloadController> _logger;
|
private readonly ILogger<DownloadController> _logger;
|
||||||
|
|
||||||
public DownloadController(IFileStore storage, ILogger<DownloadController> logger)
|
public DownloadController(IFileStore storage, ILogger<DownloadController> logger, IPaywallStore paywall)
|
||||||
{
|
{
|
||||||
_storage = storage;
|
_storage = storage;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_paywall = paywall;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpOptions]
|
[HttpOptions]
|
||||||
@ -32,8 +35,9 @@ public class DownloadController : Controller
|
|||||||
{
|
{
|
||||||
var gid = id.FromBase58Guid();
|
var gid = id.FromBase58Guid();
|
||||||
var voidFile = await SetupDownload(gid);
|
var voidFile = await SetupDownload(gid);
|
||||||
|
if (voidFile == default) return;
|
||||||
|
|
||||||
var egressReq = new EgressRequest(gid, GetRanges(Request, (long) voidFile!.Metadata!.Size));
|
var egressReq = new EgressRequest(gid, GetRanges(Request, (long)voidFile!.Metadata!.Size));
|
||||||
if (egressReq.Ranges.Count() > 1)
|
if (egressReq.Ranges.Count() > 1)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Multi-range request not supported!");
|
_logger.LogWarning("Multi-range request not supported!");
|
||||||
@ -45,10 +49,10 @@ public class DownloadController : Controller
|
|||||||
}
|
}
|
||||||
else if (egressReq.Ranges.Count() == 1)
|
else if (egressReq.Ranges.Count() == 1)
|
||||||
{
|
{
|
||||||
Response.StatusCode = (int) HttpStatusCode.PartialContent;
|
Response.StatusCode = (int)HttpStatusCode.PartialContent;
|
||||||
if (egressReq.Ranges.Sum(a => a.Size) == 0)
|
if (egressReq.Ranges.Sum(a => a.Size) == 0)
|
||||||
{
|
{
|
||||||
Response.StatusCode = (int) HttpStatusCode.RequestedRangeNotSatisfiable;
|
Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,6 +82,26 @@ public class DownloadController : Controller
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check paywall
|
||||||
|
if (meta.Paywall != default)
|
||||||
|
{
|
||||||
|
var orderId = Request.Headers.GetHeader("V-OrderId");
|
||||||
|
if (string.IsNullOrEmpty(orderId))
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var order = await _paywall.GetOrder(orderId.FromBase58Guid());
|
||||||
|
if (order?.Status != PaywallStatus.Paid)
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Response.Headers.XFrameOptions = "SAMEORIGIN";
|
Response.Headers.XFrameOptions = "SAMEORIGIN";
|
||||||
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
|
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
|
||||||
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
|
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
|
||||||
@ -110,4 +134,4 @@ public class DownloadController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
|
|||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
using VoidCat.Services;
|
using VoidCat.Model.Paywall;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Controllers
|
namespace VoidCat.Controllers
|
||||||
@ -12,10 +12,14 @@ namespace VoidCat.Controllers
|
|||||||
public class UploadController : Controller
|
public class UploadController : Controller
|
||||||
{
|
{
|
||||||
private readonly IFileStore _storage;
|
private readonly IFileStore _storage;
|
||||||
|
private readonly IFileMetadataStore _metadata;
|
||||||
|
private readonly IPaywallStore _paywall;
|
||||||
|
|
||||||
public UploadController(IFileStore storage)
|
public UploadController(IFileStore storage, IFileMetadataStore metadata, IPaywallStore paywall)
|
||||||
{
|
{
|
||||||
_storage = storage;
|
_storage = storage;
|
||||||
|
_metadata = metadata;
|
||||||
|
_paywall = paywall;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@ -78,6 +82,32 @@ namespace VoidCat.Controllers
|
|||||||
{
|
{
|
||||||
return _storage.Get(id.FromBase58Guid());
|
return _storage.Get(id.FromBase58Guid());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{id}/paywall")]
|
||||||
|
public ValueTask<PaywallOrder?> CreateOrder([FromRoute] string id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{id}/paywall")]
|
||||||
|
public async Task<IActionResult> SetPaywallConfig([FromRoute] string id, [FromBody] SetPaywallConfigRequest req)
|
||||||
|
{
|
||||||
|
var gid = id.FromBase58Guid();
|
||||||
|
var meta = await _metadata.Get(gid);
|
||||||
|
if (meta == default) return NotFound();
|
||||||
|
|
||||||
|
if (req.EditSecret != meta.EditSecret) return Unauthorized();
|
||||||
|
|
||||||
|
if (req.Strike != default)
|
||||||
|
{
|
||||||
|
await _paywall.SetConfig(gid, req.Strike!);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
@ -104,4 +134,12 @@ namespace VoidCat.Controllers
|
|||||||
public static UploadResult Error(string message)
|
public static UploadResult Error(string message)
|
||||||
=> new(false, null, message);
|
=> new(false, null, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record SetPaywallConfigRequest
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(Base58GuidConverter))]
|
||||||
|
public Guid EditSecret { get; init; }
|
||||||
|
|
||||||
|
public StrikePaywallConfig? Strike { get; init; }
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,30 +0,0 @@
|
|||||||
namespace VoidCat.Model;
|
|
||||||
|
|
||||||
public record Paywall
|
|
||||||
{
|
|
||||||
public PaywallServices Service { get; init; }
|
|
||||||
|
|
||||||
public PaywallConfig? Config { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum PaywallServices
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
Strike
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum PaywallCurrencies
|
|
||||||
{
|
|
||||||
BTC,
|
|
||||||
USD,
|
|
||||||
EUR,
|
|
||||||
GBP
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract record PaywallConfig
|
|
||||||
{
|
|
||||||
public PaywallCurrencies Currency { get; init; }
|
|
||||||
public decimal Cost { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public record StrikePaywallConfig(string Handle) : PaywallConfig;
|
|
12
VoidCat/Model/Paywall/Paywall.cs
Normal file
12
VoidCat/Model/Paywall/Paywall.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace VoidCat.Model.Paywall;
|
||||||
|
|
||||||
|
public enum PaywallServices
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Strike
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract record PaywallConfig(PaywallServices Service, [property: JsonConverter(typeof(JsonStringEnumConverter))]PaywallMoney Cost);
|
||||||
|
public record StrikePaywallConfig(string Handle, PaywallMoney Cost) : PaywallConfig(PaywallServices.Strike, Cost);
|
14
VoidCat/Model/Paywall/PaywallMoney.cs
Normal file
14
VoidCat/Model/Paywall/PaywallMoney.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace VoidCat.Model.Paywall;
|
||||||
|
|
||||||
|
public record PaywallMoney(decimal Amount, PaywallCurrencies Currency);
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public enum PaywallCurrencies : byte
|
||||||
|
{
|
||||||
|
BTC = 0,
|
||||||
|
USD = 1,
|
||||||
|
EUR = 2,
|
||||||
|
GBP = 3
|
||||||
|
}
|
11
VoidCat/Model/Paywall/PaywallOrder.cs
Normal file
11
VoidCat/Model/Paywall/PaywallOrder.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace VoidCat.Model.Paywall;
|
||||||
|
|
||||||
|
public enum PaywallStatus : byte
|
||||||
|
{
|
||||||
|
Unpaid,
|
||||||
|
Paid,
|
||||||
|
Expired
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract record PaywallOrder(Guid Id, PaywallMoney Price, PaywallStatus Status);
|
||||||
|
public record LightningPaywallOrder(Guid Id, PaywallMoney Price, PaywallStatus Status, string LnInvoice) : PaywallOrder(Id, Price, Status);
|
@ -1,4 +1,5 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using VoidCat.Model.Paywall;
|
||||||
|
|
||||||
namespace VoidCat.Model
|
namespace VoidCat.Model
|
||||||
{
|
{
|
||||||
@ -18,7 +19,7 @@ namespace VoidCat.Model
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional paywall config
|
/// Optional paywall config
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Paywall? Paywall { get; init; }
|
public PaywallConfig? Paywall { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record PublicVoidFile : VoidFile<VoidFileMeta>
|
public sealed record PublicVoidFile : VoidFile<VoidFileMeta>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
namespace VoidCat.Model
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Model
|
||||||
{
|
{
|
||||||
public class VoidSettings
|
public class VoidSettings
|
||||||
{
|
{
|
||||||
@ -9,6 +11,8 @@
|
|||||||
public JwtSettings JwtSettings { get; init; } = new("void_cat_internal", "default_key");
|
public JwtSettings JwtSettings { get; init; } = new("void_cat_internal", "default_key");
|
||||||
|
|
||||||
public string? Redis { get; init; }
|
public string? Redis { get; init; }
|
||||||
|
|
||||||
|
public StrikeApiSettings? Strike { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record TorSettings(Uri TorControl, string PrivateKey, string ControlPassword);
|
public sealed record TorSettings(Uri TorControl, string PrivateKey, string ControlPassword);
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
using VoidCat.Services;
|
using VoidCat.Services;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
using VoidCat.Services.InMemory;
|
||||||
using VoidCat.Services.Migrations;
|
using VoidCat.Services.Migrations;
|
||||||
|
using VoidCat.Services.Redis;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
var services = builder.Services;
|
var services = builder.Services;
|
||||||
@ -27,7 +30,10 @@ if (useRedis)
|
|||||||
}
|
}
|
||||||
|
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
services.AddControllers().AddNewtonsoftJson();
|
services.AddControllers().AddNewtonsoftJson((opt) =>
|
||||||
|
{
|
||||||
|
opt.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||||
|
});
|
||||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.AddJwtBearer(options =>
|
.AddJwtBearer(options =>
|
||||||
{
|
{
|
||||||
@ -53,6 +59,7 @@ if (useRedis)
|
|||||||
services.AddScoped<RedisStatsController>();
|
services.AddScoped<RedisStatsController>();
|
||||||
services.AddScoped<IStatsCollector>(svc => svc.GetRequiredService<RedisStatsController>());
|
services.AddScoped<IStatsCollector>(svc => svc.GetRequiredService<RedisStatsController>());
|
||||||
services.AddScoped<IStatsReporter>(svc => svc.GetRequiredService<RedisStatsController>());
|
services.AddScoped<IStatsReporter>(svc => svc.GetRequiredService<RedisStatsController>());
|
||||||
|
services.AddScoped<IPaywallStore, RedisPaywallStore>();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -60,6 +67,7 @@ else
|
|||||||
services.AddScoped<InMemoryStatsController>();
|
services.AddScoped<InMemoryStatsController>();
|
||||||
services.AddScoped<IStatsReporter>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
services.AddScoped<IStatsReporter>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
||||||
services.AddScoped<IStatsCollector>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
services.AddScoped<IStatsCollector>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
||||||
|
services.AddScoped<IPaywallStore, InMemoryPaywallStore>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
24
VoidCat/Services/Abstractions/IPaywallFactory.cs
Normal file
24
VoidCat/Services/Abstractions/IPaywallFactory.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using VoidCat.Model.Paywall;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
public interface IPaywallFactory
|
||||||
|
{
|
||||||
|
ValueTask<IPaywallProvider> CreateStrikeProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPaywallProvider
|
||||||
|
{
|
||||||
|
ValueTask<PaywallOrder?> CreateOrder(PaywallConfig config);
|
||||||
|
|
||||||
|
ValueTask<PaywallOrder?> GetOrderStatus(Guid id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPaywallStore
|
||||||
|
{
|
||||||
|
ValueTask<PaywallOrder?> GetOrder(Guid id);
|
||||||
|
ValueTask SaveOrder(PaywallOrder order);
|
||||||
|
|
||||||
|
ValueTask<PaywallConfig?> GetConfig(Guid id);
|
||||||
|
ValueTask SetConfig(Guid id, PaywallConfig config);
|
||||||
|
}
|
317
VoidCat/Services/Abstractions/StrikeApi.cs
Normal file
317
VoidCat/Services/Abstractions/StrikeApi.cs
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
public class StrikeApi
|
||||||
|
{
|
||||||
|
private readonly ILogger<StrikeApi> _logger;
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly StrikeApiSettings _settings;
|
||||||
|
|
||||||
|
public StrikeApi(StrikeApiSettings settings, ILogger<StrikeApi> logger)
|
||||||
|
{
|
||||||
|
_client = new HttpClient
|
||||||
|
{
|
||||||
|
BaseAddress = settings.Uri ?? new Uri("https://api.strike.me/")
|
||||||
|
};
|
||||||
|
|
||||||
|
_settings = settings;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {settings.ApiKey}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Invoice?> GenerateInvoice(CreateInvoiceRequest invoiceRequest)
|
||||||
|
{
|
||||||
|
var path = !string.IsNullOrEmpty(invoiceRequest.Handle)
|
||||||
|
? $"/v1/invoices/handle/{invoiceRequest.Handle}"
|
||||||
|
: "/v1/invoices";
|
||||||
|
|
||||||
|
return SendRequest<Invoice>(HttpMethod.Post, path, invoiceRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Profile?> GetProfile(string handle)
|
||||||
|
{
|
||||||
|
return SendRequest<Profile>(HttpMethod.Get, $"/v1/accounts/handle/{handle}/profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Profile?> GetProfile(Guid id)
|
||||||
|
{
|
||||||
|
return SendRequest<Profile>(HttpMethod.Get, $"/v1/accounts/{id}/profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Invoice?> GetInvoice(Guid id)
|
||||||
|
{
|
||||||
|
return SendRequest<Invoice>(HttpMethod.Get, $"/v1/invoices/{id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<InvoiceQuote?> GetInvoiceQuote(Guid id)
|
||||||
|
{
|
||||||
|
return SendRequest<InvoiceQuote>(HttpMethod.Post, $"/v1/invoices/{id}/quote");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<WebhookSubscription>?> GetWebhookSubscriptions()
|
||||||
|
{
|
||||||
|
return SendRequest<IEnumerable<WebhookSubscription>>(HttpMethod.Get, "/v1/subscriptions");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<WebhookSubscription?> CreateWebhook(NewWebhook hook)
|
||||||
|
{
|
||||||
|
return SendRequest<WebhookSubscription>(HttpMethod.Post, "/v1/subscriptions", hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteWebhook(Guid id)
|
||||||
|
{
|
||||||
|
return SendRequest<object>(HttpMethod.Delete, $"/v1/subscriptions/{id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TReturn?> SendRequest<TReturn>(HttpMethod method, string path, object? bodyObj = default)
|
||||||
|
where TReturn : class
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(method, path);
|
||||||
|
if (bodyObj != default)
|
||||||
|
{
|
||||||
|
var reqJson = JsonConvert.SerializeObject(bodyObj);
|
||||||
|
request.Content = new StringContent(reqJson, Encoding.UTF8, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
var rsp = await _client.SendAsync(request);
|
||||||
|
var okResponse = method.Method switch
|
||||||
|
{
|
||||||
|
"POST" => HttpStatusCode.Created,
|
||||||
|
_ => HttpStatusCode.OK
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = await rsp.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogInformation(json);
|
||||||
|
return rsp.StatusCode == okResponse ? JsonConvert.DeserializeObject<TReturn>(json) : default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Profile
|
||||||
|
{
|
||||||
|
[JsonProperty("handle")]
|
||||||
|
public string Handle { get; init; } = null;
|
||||||
|
|
||||||
|
[JsonProperty("avatarUrl")]
|
||||||
|
public string? AvatarUrl { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("description")]
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("canReceive")]
|
||||||
|
public bool CanReceive { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("currencies")]
|
||||||
|
public List<AvailableCurrency> Currencies { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InvoiceQuote
|
||||||
|
{
|
||||||
|
[JsonProperty("quoteId")]
|
||||||
|
public Guid QuoteId { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("description")]
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("lnInvoice")]
|
||||||
|
public string? LnInvoice { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("onchainAddress")]
|
||||||
|
public string? OnChainAddress { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("expiration")]
|
||||||
|
public DateTimeOffset Expiration { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("expirationInSec")]
|
||||||
|
public ulong ExpirationSec { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("targetAmount")]
|
||||||
|
public CurrencyAmount? TargetAmount { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("sourceAmount")]
|
||||||
|
public CurrencyAmount? SourceAmount { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("conversionRate")]
|
||||||
|
public ConversionRate? ConversionRate { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConversionRate
|
||||||
|
{
|
||||||
|
[JsonProperty("amount")]
|
||||||
|
public string? Amount { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("sourceCurrency")]
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public Currencies Source { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("targetCurrency")]
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public Currencies Target { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ErrorResponse : Exception
|
||||||
|
{
|
||||||
|
public ErrorResponse(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateInvoiceRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("correlationId")]
|
||||||
|
public string? CorrelationId { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("description")]
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("amount")]
|
||||||
|
public CurrencyAmount? Amount { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("handle")]
|
||||||
|
public string? Handle { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CurrencyAmount
|
||||||
|
{
|
||||||
|
[JsonProperty("amount")]
|
||||||
|
public string? Amount { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("currency")]
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public Currencies? Currency { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AvailableCurrency
|
||||||
|
{
|
||||||
|
[JsonProperty("currency")]
|
||||||
|
public Currencies Currency { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("isDefaultCurrency")]
|
||||||
|
public bool IsDefault { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("isAvailable")]
|
||||||
|
public bool IsAvailable { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Currencies
|
||||||
|
{
|
||||||
|
BTC,
|
||||||
|
USD,
|
||||||
|
EUR,
|
||||||
|
GBP,
|
||||||
|
USDT
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Invoice
|
||||||
|
{
|
||||||
|
[JsonProperty("invoiceId")]
|
||||||
|
public Guid InvoiceId { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("amount")]
|
||||||
|
public CurrencyAmount? Amount { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("state")]
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public InvoiceState State { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("created")]
|
||||||
|
public DateTimeOffset? Created { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("correlationId")]
|
||||||
|
public string? CorrelationId { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("description")]
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("issuerId")]
|
||||||
|
public Guid? IssuerId { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("receiverId")]
|
||||||
|
public Guid? ReceiverId { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("payerId")]
|
||||||
|
public Guid? PayerId { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class WebhookBase
|
||||||
|
{
|
||||||
|
[JsonProperty("webhookUrl")]
|
||||||
|
public Uri? Uri { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("webhookVersion")]
|
||||||
|
public string? Version { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("enabled")]
|
||||||
|
public bool? Enabled { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("eventTypes")]
|
||||||
|
public HashSet<string>? EventTypes { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class NewWebhook : WebhookBase
|
||||||
|
{
|
||||||
|
[JsonProperty("secret")]
|
||||||
|
public string? Secret { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WebhookSubscription : WebhookBase
|
||||||
|
{
|
||||||
|
[JsonProperty("id")]
|
||||||
|
public Guid? Id { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("created")]
|
||||||
|
public DateTimeOffset? Created { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebhookData
|
||||||
|
{
|
||||||
|
[JsonProperty("entityId")]
|
||||||
|
public Guid? EntityId { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("changes")]
|
||||||
|
public List<string>? Changes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebhookEvent
|
||||||
|
{
|
||||||
|
[JsonProperty("id")]
|
||||||
|
public Guid? Id { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("eventType")]
|
||||||
|
public string? EventType { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("webhookVersion")]
|
||||||
|
public string? WebhookVersion { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("data")]
|
||||||
|
public WebhookData? Data { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("created")]
|
||||||
|
public DateTimeOffset? Created { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("deliverySuccess")]
|
||||||
|
public bool? DeliverySuccess { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Id = {Id}, EntityId = {Data?.EntityId}, Event = {EventType}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum InvoiceState
|
||||||
|
{
|
||||||
|
UNPAID,
|
||||||
|
PENDING,
|
||||||
|
PAID,
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StrikeApiSettings
|
||||||
|
{
|
||||||
|
public Uri? Uri { get; init; }
|
||||||
|
public string? ApiKey { get; init; }
|
||||||
|
}
|
37
VoidCat/Services/InMemory/InMemoryPaywallStore.cs
Normal file
37
VoidCat/Services/InMemory/InMemoryPaywallStore.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using VoidCat.Model.Paywall;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.InMemory;
|
||||||
|
|
||||||
|
public class InMemoryPaywallStore : IPaywallStore
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
|
public InMemoryPaywallStore(IMemoryCache cache)
|
||||||
|
{
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<PaywallConfig?> GetConfig(Guid id)
|
||||||
|
{
|
||||||
|
return ValueTask.FromResult(_cache.Get(id) as PaywallConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask SetConfig(Guid id, PaywallConfig config)
|
||||||
|
{
|
||||||
|
_cache.Set(id, config);
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<PaywallOrder?> GetOrder(Guid id)
|
||||||
|
{
|
||||||
|
return ValueTask.FromResult(_cache.Get(id) as PaywallOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask SaveOrder(PaywallOrder order)
|
||||||
|
{
|
||||||
|
_cache.Set(order.Id, order);
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Services;
|
namespace VoidCat.Services.InMemory;
|
||||||
|
|
||||||
public class InMemoryStatsController : IStatsCollector, IStatsReporter
|
public class InMemoryStatsController : IStatsCollector, IStatsReporter
|
||||||
{
|
{
|
@ -12,13 +12,15 @@ public class LocalDiskFileStore : IFileStore
|
|||||||
private readonly VoidSettings _settings;
|
private readonly VoidSettings _settings;
|
||||||
private readonly IAggregateStatsCollector _stats;
|
private readonly IAggregateStatsCollector _stats;
|
||||||
private readonly IFileMetadataStore _metadataStore;
|
private readonly IFileMetadataStore _metadataStore;
|
||||||
|
private readonly IPaywallStore _paywallStore;
|
||||||
|
|
||||||
public LocalDiskFileStore(VoidSettings settings, IAggregateStatsCollector stats,
|
public LocalDiskFileStore(VoidSettings settings, IAggregateStatsCollector stats,
|
||||||
IFileMetadataStore metadataStore)
|
IFileMetadataStore metadataStore, IPaywallStore paywallStore)
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_stats = stats;
|
_stats = stats;
|
||||||
_metadataStore = metadataStore;
|
_metadataStore = metadataStore;
|
||||||
|
_paywallStore = paywallStore;
|
||||||
|
|
||||||
if (!Directory.Exists(_settings.DataDirectory))
|
if (!Directory.Exists(_settings.DataDirectory))
|
||||||
{
|
{
|
||||||
@ -31,7 +33,8 @@ public class LocalDiskFileStore : IFileStore
|
|||||||
return new ()
|
return new ()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Metadata = await _metadataStore.GetPublic(id)
|
Metadata = await _metadataStore.GetPublic(id),
|
||||||
|
Paywall = await _paywallStore.GetConfig(id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,6 @@ public class WrongFile
|
|||||||
[JsonConverter(typeof(Base58GuidConverter))]
|
[JsonConverter(typeof(Base58GuidConverter))]
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public WrongMeta? Metadata { get; init; }
|
public WrongMeta? Metadata { get; init; }
|
||||||
public Paywall? Paywall { get; init; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WrongMeta
|
public class WrongMeta
|
||||||
|
46
VoidCat/Services/Redis/RedisPaywallStore.cs
Normal file
46
VoidCat/Services/Redis/RedisPaywallStore.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using VoidCat.Model.Paywall;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Redis;
|
||||||
|
|
||||||
|
public class RedisPaywallStore : IPaywallStore
|
||||||
|
{
|
||||||
|
private readonly IDatabase _database;
|
||||||
|
|
||||||
|
public RedisPaywallStore(IDatabase database)
|
||||||
|
{
|
||||||
|
_database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<PaywallConfig?> GetConfig(Guid id)
|
||||||
|
{
|
||||||
|
var json = await _database.StringGetAsync(ConfigKey(id));
|
||||||
|
var cfg = JsonConvert.DeserializeObject<PaywallConfig>(json);
|
||||||
|
return cfg?.Service switch
|
||||||
|
{
|
||||||
|
PaywallServices.Strike => JsonConvert.DeserializeObject<StrikePaywallConfig>(json),
|
||||||
|
_ => default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask SetConfig(Guid id, PaywallConfig config)
|
||||||
|
{
|
||||||
|
await _database.StringSetAsync(ConfigKey(id), JsonConvert.SerializeObject(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<PaywallOrder?> GetOrder(Guid id)
|
||||||
|
{
|
||||||
|
var json = await _database.StringGetAsync(OrderKey(id));
|
||||||
|
return JsonConvert.DeserializeObject<PaywallOrder>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask SaveOrder(PaywallOrder order)
|
||||||
|
{
|
||||||
|
await _database.StringSetAsync(OrderKey(order.Id), JsonConvert.SerializeObject(order));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RedisKey ConfigKey(Guid id) => $"paywall:config:{id}";
|
||||||
|
private RedisKey OrderKey(Guid id) => $"paywall:order:{id}";
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Services;
|
namespace VoidCat.Services.Redis;
|
||||||
|
|
||||||
public class RedisStatsController : IStatsReporter, IStatsCollector
|
public class RedisStatsController : IStatsReporter, IStatsCollector
|
||||||
{
|
{
|
@ -29,4 +29,11 @@ export const ZiB = Math.pow(1024, 7);
|
|||||||
/**
|
/**
|
||||||
* @constant {number} - Size of 1 YiB
|
* @constant {number} - Size of 1 YiB
|
||||||
*/
|
*/
|
||||||
export const YiB = Math.pow(1024, 8);
|
export const YiB = Math.pow(1024, 8);
|
||||||
|
|
||||||
|
export const PaywallCurrencies = {
|
||||||
|
BTC: 0,
|
||||||
|
USD: 1,
|
||||||
|
EUR: 2,
|
||||||
|
GBP: 3
|
||||||
|
}
|
9
VoidCat/spa/src/FileEdit.css
Normal file
9
VoidCat/spa/src/FileEdit.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.file-edit {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-edit > div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
47
VoidCat/spa/src/FileEdit.js
Normal file
47
VoidCat/spa/src/FileEdit.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
|
import "./FileEdit.css";
|
||||||
|
import {StrikePaywallConfig} from "./StrikePaywallConfig";
|
||||||
|
|
||||||
|
export function FileEdit(props) {
|
||||||
|
const [paywall, setPaywall] = useState();
|
||||||
|
|
||||||
|
const privateFile = JSON.parse(window.localStorage.getItem(props.file.id));
|
||||||
|
if (!privateFile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPaywallConfig() {
|
||||||
|
switch (paywall) {
|
||||||
|
case 1: {
|
||||||
|
return <StrikePaywallConfig file={privateFile}/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = props.file.metadata;
|
||||||
|
return (
|
||||||
|
<div className="file-edit">
|
||||||
|
<div>
|
||||||
|
<h3>File info</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Filename:</dt>
|
||||||
|
<dd><input type="text" value={meta.name}/></dd>
|
||||||
|
<dt>Description:</dt>
|
||||||
|
<dd><input type="text" value={meta.description}/></dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Paywall Config</h3>
|
||||||
|
Type:
|
||||||
|
<select onChange={(e) => setPaywall(parseInt(e.target.value))}>
|
||||||
|
<option value={0}>None</option>
|
||||||
|
<option value={1}>Strike</option>
|
||||||
|
</select>
|
||||||
|
{renderPaywallConfig()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
21
VoidCat/spa/src/FilePaywall.js
Normal file
21
VoidCat/spa/src/FilePaywall.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {ConstName} from "./Util";
|
||||||
|
import {PaywallCurrencies} from "./Const";
|
||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
|
export function FilePaywall(props) {
|
||||||
|
const file = props.file;
|
||||||
|
const pw = file.paywall;
|
||||||
|
|
||||||
|
const [order, setOrder] = useState();
|
||||||
|
|
||||||
|
async function fetchOrder() {
|
||||||
|
let req = await fetch("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="paywall">
|
||||||
|
<h3>You must pay {ConstName(PaywallCurrencies, pw.cost.currency)} {pw.cost.amount} to view this file.</h3>
|
||||||
|
<button onClick={fetchOrder}>Pay</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -3,6 +3,8 @@ import {useParams} from "react-router-dom";
|
|||||||
import {TextPreview} from "./TextPreview";
|
import {TextPreview} from "./TextPreview";
|
||||||
|
|
||||||
import "./FilePreview.css";
|
import "./FilePreview.css";
|
||||||
|
import {FileEdit} from "./FileEdit";
|
||||||
|
import {FilePaywall} from "./FilePaywall";
|
||||||
|
|
||||||
export function FilePreview() {
|
export function FilePreview() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -17,6 +19,10 @@ export function FilePreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTypes() {
|
function renderTypes() {
|
||||||
|
if(info.paywall) {
|
||||||
|
return <FilePaywall file={info}/>;
|
||||||
|
}
|
||||||
|
|
||||||
let link = `/d/${info.id}`;
|
let link = `/d/${info.id}`;
|
||||||
if (info.metadata) {
|
if (info.metadata) {
|
||||||
switch (info.metadata.mimeType) {
|
switch (info.metadata.mimeType) {
|
||||||
@ -53,6 +59,7 @@ export function FilePreview() {
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
this.Download(<a className="btn" href={`/d/${info.id}`}>{info.metadata?.name ?? info.id}</a>)
|
this.Download(<a className="btn" href={`/d/${info.id}`}>{info.metadata?.name ?? info.id}</a>)
|
||||||
{renderTypes()}
|
{renderTypes()}
|
||||||
|
<FileEdit file={info}/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : "Not Found"}
|
) : "Not Found"}
|
||||||
</div>
|
</div>
|
||||||
|
@ -139,6 +139,7 @@ export function FileUpload(props) {
|
|||||||
if (xhr.ok) {
|
if (xhr.ok) {
|
||||||
setUState(UploadState.Done);
|
setUState(UploadState.Done);
|
||||||
setResult(xhr.file);
|
setResult(xhr.file);
|
||||||
|
window.localStorage.setItem(xhr.file.id, JSON.stringify(xhr.file));
|
||||||
} else {
|
} else {
|
||||||
setUState(UploadState.Failed);
|
setUState(UploadState.Failed);
|
||||||
setResult(xhr.errorMessage);
|
setResult(xhr.errorMessage);
|
||||||
|
56
VoidCat/spa/src/StrikePaywallConfig.js
Normal file
56
VoidCat/spa/src/StrikePaywallConfig.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import {useState} from "react";
|
||||||
|
import {PaywallCurrencies} from "./Const";
|
||||||
|
|
||||||
|
export function StrikePaywallConfig(props) {
|
||||||
|
const editSecret = props.file.metadata.editSecret;
|
||||||
|
const id = props.file.id;
|
||||||
|
|
||||||
|
const [username, setUsername] = useState("hrf");
|
||||||
|
const [currency, setCurrency] = useState(PaywallCurrencies.USD);
|
||||||
|
const [price, setPrice] = useState(1);
|
||||||
|
|
||||||
|
async function saveStrikeConfig() {
|
||||||
|
let cfg = {
|
||||||
|
editSecret,
|
||||||
|
strike: {
|
||||||
|
handle: username,
|
||||||
|
cost: {
|
||||||
|
currency: currency,
|
||||||
|
amount: price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = await fetch(`/upload/${id}/paywall`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(cfg),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!req.ok) {
|
||||||
|
alert("Error settings paywall config!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<dl>
|
||||||
|
<dt>Stike username:</dt>
|
||||||
|
<dd><input type="text" value={username} onChange={(e) => setUsername(e.target.value)}/></dd>
|
||||||
|
<dt>Currency:</dt>
|
||||||
|
<dd>
|
||||||
|
<select onChange={(e) => setCurrency(parseInt(e.target.value))}>
|
||||||
|
<option selected={currency === PaywallCurrencies.BTC} value={PaywallCurrencies.BTC}>BTC</option>
|
||||||
|
<option selected={currency === PaywallCurrencies.USD} value={PaywallCurrencies.USD}>USD</option>
|
||||||
|
<option selected={currency === PaywallCurrencies.EUR} value={PaywallCurrencies.EUR}>EUR</option>
|
||||||
|
<option selected={currency === PaywallCurrencies.GBP} value={PaywallCurrencies.GBP}>GBP</option>
|
||||||
|
</select>
|
||||||
|
</dd>
|
||||||
|
<dt>Price:</dt>
|
||||||
|
<dd><input type="number" value={price} onChange={(e) => setPrice(parseFloat(e.target.value))}/></dd>
|
||||||
|
</dl>
|
||||||
|
<button onClick={saveStrikeConfig}>Save</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user