Add strike paywall (incomplete)

This commit is contained in:
Kieran 2022-02-21 09:39:59 +00:00
parent e098b2c0f0
commit 0eda25ba00
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
24 changed files with 702 additions and 46 deletions

View File

@ -1,6 +1,7 @@
using System.Net;
using Microsoft.AspNetCore.Mvc;
using VoidCat.Model;
using VoidCat.Model.Paywall;
using VoidCat.Services.Abstractions;
namespace VoidCat.Controllers;
@ -9,12 +10,14 @@ namespace VoidCat.Controllers;
public class DownloadController : Controller
{
private readonly IFileStore _storage;
private readonly IPaywallStore _paywall;
private readonly ILogger<DownloadController> _logger;
public DownloadController(IFileStore storage, ILogger<DownloadController> logger)
public DownloadController(IFileStore storage, ILogger<DownloadController> logger, IPaywallStore paywall)
{
_storage = storage;
_logger = logger;
_paywall = paywall;
}
[HttpOptions]
@ -32,8 +35,9 @@ public class DownloadController : Controller
{
var gid = id.FromBase58Guid();
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)
{
_logger.LogWarning("Multi-range request not supported!");
@ -45,10 +49,10 @@ public class DownloadController : Controller
}
else if (egressReq.Ranges.Count() == 1)
{
Response.StatusCode = (int) HttpStatusCode.PartialContent;
Response.StatusCode = (int)HttpStatusCode.PartialContent;
if (egressReq.Ranges.Sum(a => a.Size) == 0)
{
Response.StatusCode = (int) HttpStatusCode.RequestedRangeNotSatisfiable;
Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
return;
}
}
@ -78,6 +82,26 @@ public class DownloadController : Controller
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.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";

View File

@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
using VoidCat.Model;
using VoidCat.Services;
using VoidCat.Model.Paywall;
using VoidCat.Services.Abstractions;
namespace VoidCat.Controllers
@ -12,10 +12,14 @@ namespace VoidCat.Controllers
public class UploadController : Controller
{
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;
_metadata = metadata;
_paywall = paywall;
}
[HttpPost]
@ -78,6 +82,32 @@ namespace VoidCat.Controllers
{
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)]
@ -104,4 +134,12 @@ namespace VoidCat.Controllers
public static UploadResult Error(string message)
=> new(false, null, message);
}
public record SetPaywallConfigRequest
{
[JsonConverter(typeof(Base58GuidConverter))]
public Guid EditSecret { get; init; }
public StrikePaywallConfig? Strike { get; init; }
}
}

View File

@ -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;

View 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);

View 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
}

View 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);

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using VoidCat.Model.Paywall;
namespace VoidCat.Model
{
@ -18,7 +19,7 @@ namespace VoidCat.Model
/// <summary>
/// Optional paywall config
/// </summary>
public Paywall? Paywall { get; init; }
public PaywallConfig? Paywall { get; init; }
}
public sealed record PublicVoidFile : VoidFile<VoidFileMeta>

View File

@ -1,4 +1,6 @@
namespace VoidCat.Model
using VoidCat.Services.Abstractions;
namespace VoidCat.Model
{
public class VoidSettings
{
@ -9,6 +11,8 @@
public JwtSettings JwtSettings { get; init; } = new("void_cat_internal", "default_key");
public string? Redis { get; init; }
public StrikeApiSettings? Strike { get; init; }
}
public sealed record TorSettings(Uri TorControl, string PrivateKey, string ControlPassword);

View File

@ -1,12 +1,15 @@
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Prometheus;
using StackExchange.Redis;
using VoidCat.Model;
using VoidCat.Services;
using VoidCat.Services.Abstractions;
using VoidCat.Services.InMemory;
using VoidCat.Services.Migrations;
using VoidCat.Services.Redis;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
@ -27,7 +30,10 @@ if (useRedis)
}
services.AddRouting();
services.AddControllers().AddNewtonsoftJson();
services.AddControllers().AddNewtonsoftJson((opt) =>
{
opt.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
@ -53,6 +59,7 @@ if (useRedis)
services.AddScoped<RedisStatsController>();
services.AddScoped<IStatsCollector>(svc => svc.GetRequiredService<RedisStatsController>());
services.AddScoped<IStatsReporter>(svc => svc.GetRequiredService<RedisStatsController>());
services.AddScoped<IPaywallStore, RedisPaywallStore>();
}
else
{
@ -60,6 +67,7 @@ else
services.AddScoped<InMemoryStatsController>();
services.AddScoped<IStatsReporter>(svc => svc.GetRequiredService<InMemoryStatsController>());
services.AddScoped<IStatsCollector>(svc => svc.GetRequiredService<InMemoryStatsController>());
services.AddScoped<IPaywallStore, InMemoryPaywallStore>();
}
var app = builder.Build();

View 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);
}

View 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; }
}

View 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;
}
}

View File

@ -1,7 +1,7 @@
using Microsoft.Extensions.Caching.Memory;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services;
namespace VoidCat.Services.InMemory;
public class InMemoryStatsController : IStatsCollector, IStatsReporter
{

View File

@ -12,13 +12,15 @@ public class LocalDiskFileStore : IFileStore
private readonly VoidSettings _settings;
private readonly IAggregateStatsCollector _stats;
private readonly IFileMetadataStore _metadataStore;
private readonly IPaywallStore _paywallStore;
public LocalDiskFileStore(VoidSettings settings, IAggregateStatsCollector stats,
IFileMetadataStore metadataStore)
IFileMetadataStore metadataStore, IPaywallStore paywallStore)
{
_settings = settings;
_stats = stats;
_metadataStore = metadataStore;
_paywallStore = paywallStore;
if (!Directory.Exists(_settings.DataDirectory))
{
@ -31,7 +33,8 @@ public class LocalDiskFileStore : IFileStore
return new ()
{
Id = id,
Metadata = await _metadataStore.GetPublic(id)
Metadata = await _metadataStore.GetPublic(id),
Paywall = await _paywallStore.GetConfig(id)
};
}

View File

@ -29,7 +29,6 @@ public class WrongFile
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
public WrongMeta? Metadata { get; init; }
public Paywall? Paywall { get; init; }
}
public class WrongMeta

View 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}";
}

View File

@ -1,7 +1,7 @@
using StackExchange.Redis;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services;
namespace VoidCat.Services.Redis;
public class RedisStatsController : IStatsReporter, IStatsCollector
{

View File

@ -30,3 +30,10 @@ export const ZiB = Math.pow(1024, 7);
* @constant {number} - Size of 1 YiB
*/
export const YiB = Math.pow(1024, 8);
export const PaywallCurrencies = {
BTC: 0,
USD: 1,
EUR: 2,
GBP: 3
}

View File

@ -0,0 +1,9 @@
.file-edit {
display: flex;
flex-direction: row;
text-align: start;
}
.file-edit > div {
flex: 1;
}

View 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>
);
}

View 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>
);
}

View File

@ -3,6 +3,8 @@ import {useParams} from "react-router-dom";
import {TextPreview} from "./TextPreview";
import "./FilePreview.css";
import {FileEdit} from "./FileEdit";
import {FilePaywall} from "./FilePaywall";
export function FilePreview() {
const params = useParams();
@ -17,6 +19,10 @@ export function FilePreview() {
}
function renderTypes() {
if(info.paywall) {
return <FilePaywall file={info}/>;
}
let link = `/d/${info.id}`;
if (info.metadata) {
switch (info.metadata.mimeType) {
@ -53,6 +59,7 @@ export function FilePreview() {
<Fragment>
this.Download(<a className="btn" href={`/d/${info.id}`}>{info.metadata?.name ?? info.id}</a>)
{renderTypes()}
<FileEdit file={info}/>
</Fragment>
) : "Not Found"}
</div>

View File

@ -139,6 +139,7 @@ export function FileUpload(props) {
if (xhr.ok) {
setUState(UploadState.Done);
setResult(xhr.file);
window.localStorage.setItem(xhr.file.id, JSON.stringify(xhr.file));
} else {
setUState(UploadState.Failed);
setResult(xhr.errorMessage);

View 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>
);
}