forked from Kieran/void.cat
Page file list response
Standardize api calls
This commit is contained in:
parent
49c6aaa249
commit
f5e3b47311
@ -16,11 +16,11 @@ public class AdminController : Controller
|
|||||||
_fileStore = fileStore;
|
_fileStore = fileStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpPost]
|
||||||
[Route("file")]
|
[Route("file")]
|
||||||
public IAsyncEnumerable<PublicVoidFile> ListFiles()
|
public Task<RenderedResults<PublicVoidFile>> ListFiles([FromBody] PagedRequest request)
|
||||||
{
|
{
|
||||||
return _fileStore.ListFiles();
|
return _fileStore.ListFiles(request).GetResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -26,6 +27,12 @@ public class AuthController : Controller
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (!TryValidateModel(req))
|
||||||
|
{
|
||||||
|
var error = ControllerContext.ModelState.FirstOrDefault().Value?.Errors.FirstOrDefault()?.ErrorMessage;
|
||||||
|
return new(null, error);
|
||||||
|
}
|
||||||
|
|
||||||
var user = await _manager.Login(req.Username, req.Password);
|
var user = await _manager.Login(req.Username, req.Password);
|
||||||
var token = CreateToken(user);
|
var token = CreateToken(user);
|
||||||
var tokenWriter = new JwtSecurityTokenHandler();
|
var tokenWriter = new JwtSecurityTokenHandler();
|
||||||
@ -43,6 +50,12 @@ public class AuthController : Controller
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (!TryValidateModel(req))
|
||||||
|
{
|
||||||
|
var error = ControllerContext.ModelState.FirstOrDefault().Value?.Errors.FirstOrDefault()?.ErrorMessage;
|
||||||
|
return new(null, error);
|
||||||
|
}
|
||||||
|
|
||||||
var newUser = await _manager.Register(req.Username, req.Password);
|
var newUser = await _manager.Register(req.Username, req.Password);
|
||||||
var token = CreateToken(newUser);
|
var token = CreateToken(newUser);
|
||||||
var tokenWriter = new JwtSecurityTokenHandler();
|
var tokenWriter = new JwtSecurityTokenHandler();
|
||||||
@ -72,7 +85,22 @@ public class AuthController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public record LoginRequest(string Username, string Password);
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
public LoginRequest(string username, string password)
|
||||||
|
{
|
||||||
|
Username = username;
|
||||||
|
Password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Username { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MinLength(6)]
|
||||||
|
public string Password { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public record LoginResponse(string? Jwt, string? Error = null);
|
public record LoginResponse(string? Jwt, string? Error = null);
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,8 @@ namespace VoidCat.Controllers
|
|||||||
var bw = await _statsReporter.GetBandwidth();
|
var bw = await _statsReporter.GetBandwidth();
|
||||||
var bytes = 0UL;
|
var bytes = 0UL;
|
||||||
var count = 0;
|
var count = 0;
|
||||||
await foreach (var vf in _fileStore.ListFiles())
|
var files = _fileStore.ListFiles(new(0, Int32.MaxValue));
|
||||||
|
await foreach (var vf in files.Results)
|
||||||
{
|
{
|
||||||
bytes += vf.Metadata?.Size ?? 0;
|
bytes += vf.Metadata?.Size ?? 0;
|
||||||
count++;
|
count++;
|
||||||
|
46
VoidCat/Model/PagedResult.cs
Normal file
46
VoidCat/Model/PagedResult.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
namespace VoidCat.Model;
|
||||||
|
|
||||||
|
public abstract class PagedResult
|
||||||
|
{
|
||||||
|
public int Page { get; init; }
|
||||||
|
public int PageSize { get; init; }
|
||||||
|
public int Pages => TotalResults / PageSize;
|
||||||
|
public int TotalResults { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PagedResult<T> : PagedResult
|
||||||
|
{
|
||||||
|
public IAsyncEnumerable<T> Results { get; init; }
|
||||||
|
|
||||||
|
public async Task<RenderedResults<T>> GetResults()
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Page = Page,
|
||||||
|
PageSize = PageSize,
|
||||||
|
TotalResults = TotalResults,
|
||||||
|
Results = await Results.ToListAsync()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RenderedResults<T> : PagedResult
|
||||||
|
{
|
||||||
|
public IList<T> Results { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PagedRequest(int Page, int PageSize, PagedSortBy SortBy = PagedSortBy.Name, PageSortOrder SortOrder = PageSortOrder.Asc);
|
||||||
|
|
||||||
|
public enum PagedSortBy : byte
|
||||||
|
{
|
||||||
|
Name,
|
||||||
|
Date,
|
||||||
|
Size,
|
||||||
|
Id
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PageSortOrder : byte
|
||||||
|
{
|
||||||
|
Asc,
|
||||||
|
Dsc
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
namespace VoidCat.Services.Abstractions;
|
namespace VoidCat.Model;
|
||||||
|
|
||||||
public sealed record RangeRequest(long? TotalSize, long? Start, long? End)
|
public sealed record RangeRequest(long? TotalSize, long? Start, long? End)
|
||||||
{
|
{
|
@ -10,7 +10,7 @@ public interface IFileStore
|
|||||||
|
|
||||||
ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts);
|
ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts);
|
||||||
|
|
||||||
IAsyncEnumerable<PublicVoidFile> ListFiles();
|
PagedResult<PublicVoidFile> ListFiles(PagedRequest request);
|
||||||
|
|
||||||
ValueTask DeleteFile(Guid id);
|
ValueTask DeleteFile(Guid id);
|
||||||
}
|
}
|
@ -34,12 +34,17 @@ public class LocalDiskFileStore : IFileStore
|
|||||||
|
|
||||||
public async ValueTask<PublicVoidFile?> Get(Guid id)
|
public async ValueTask<PublicVoidFile?> Get(Guid id)
|
||||||
{
|
{
|
||||||
|
var meta = _metadataStore.GetPublic(id);
|
||||||
|
var paywall = _paywallStore.GetConfig(id);
|
||||||
|
var bandwidth = _statsReporter.GetBandwidth(id);
|
||||||
|
await Task.WhenAll(meta.AsTask(), paywall.AsTask(), bandwidth.AsTask());
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Metadata = await _metadataStore.GetPublic(id),
|
Metadata = meta.Result,
|
||||||
Paywall = await _paywallStore.GetConfig(id),
|
Paywall = paywall.Result,
|
||||||
Bandwidth = await _statsReporter.GetBandwidth(id)
|
Bandwidth = bandwidth.Result
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,20 +119,45 @@ public class LocalDiskFileStore : IFileStore
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<PublicVoidFile> ListFiles()
|
public PagedResult<PublicVoidFile> ListFiles(PagedRequest request)
|
||||||
{
|
{
|
||||||
foreach (var fe in Directory.EnumerateFiles(_settings.DataDirectory))
|
var files = Directory.EnumerateFiles(_settings.DataDirectory)
|
||||||
|
.Where(a => !Path.HasExtension(a));
|
||||||
|
files = (request.SortBy, request.SortOrder) switch
|
||||||
{
|
{
|
||||||
var filename = Path.GetFileNameWithoutExtension(fe);
|
(PagedSortBy.Id, PageSortOrder.Asc) => files.OrderBy(a =>
|
||||||
if (Path.HasExtension(fe)) continue; // real file does not have extension
|
Guid.TryParse(Path.GetFileNameWithoutExtension(a), out var g) ? g : Guid.Empty),
|
||||||
if (!Guid.TryParse(filename, out var id)) continue;
|
(PagedSortBy.Id, PageSortOrder.Dsc) => files.OrderByDescending(a =>
|
||||||
|
Guid.TryParse(Path.GetFileNameWithoutExtension(a), out var g) ? g : Guid.Empty),
|
||||||
|
(PagedSortBy.Name, PageSortOrder.Asc) => files.OrderBy(Path.GetFileNameWithoutExtension),
|
||||||
|
(PagedSortBy.Name, PageSortOrder.Dsc) => files.OrderByDescending(Path.GetFileNameWithoutExtension),
|
||||||
|
(PagedSortBy.Size, PageSortOrder.Asc) => files.OrderBy(a => new FileInfo(a).Length),
|
||||||
|
(PagedSortBy.Size, PageSortOrder.Dsc) => files.OrderByDescending(a => new FileInfo(a).Length),
|
||||||
|
(PagedSortBy.Date, PageSortOrder.Asc) => files.OrderBy(File.GetCreationTimeUtc),
|
||||||
|
(PagedSortBy.Date, PageSortOrder.Dsc) => files.OrderByDescending(File.GetCreationTimeUtc),
|
||||||
|
_ => files
|
||||||
|
};
|
||||||
|
|
||||||
var vf = await Get(id);
|
async IAsyncEnumerable<PublicVoidFile> EnumeratePage(IEnumerable<string> page)
|
||||||
if (vf != default)
|
{
|
||||||
|
foreach (var file in page)
|
||||||
{
|
{
|
||||||
yield return vf;
|
if (!Guid.TryParse(Path.GetFileNameWithoutExtension(file), out var gid)) continue;
|
||||||
|
var loaded = await Get(gid);
|
||||||
|
if (loaded != default)
|
||||||
|
{
|
||||||
|
yield return loaded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Page = request.Page,
|
||||||
|
PageSize = request.PageSize,
|
||||||
|
TotalResults = files.Count(),
|
||||||
|
Results = EnumeratePage(files.Skip(request.PageSize * request.Page).Take(request.PageSize))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DeleteFile(Guid id)
|
public async ValueTask DeleteFile(Guid id)
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="5.0.2" />
|
<PackageReference Include="prometheus-net.AspNetCore" Version="5.0.2" />
|
||||||
<PackageReference Include="Seq.Extensions.Logging" Version="6.0.0" />
|
<PackageReference Include="Seq.Extensions.Logging" Version="6.0.0" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.5.27-prerelease" />
|
<PackageReference Include="StackExchange.Redis" Version="2.5.27-prerelease" />
|
||||||
|
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Don't publish the SPA source files, but do show them in the project files list -->
|
<!-- Don't publish the SPA source files, but do show them in the project files list -->
|
||||||
|
@ -1,41 +1,45 @@
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {useSelector} from "react-redux";
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {FormatBytes} from "../Util";
|
import {FormatBytes} from "../Util";
|
||||||
|
|
||||||
import "./FileList.css";
|
import "./FileList.css";
|
||||||
|
import {AdminApi} from "../Api";
|
||||||
|
import {logout} from "../LoginState";
|
||||||
|
import {PagedSortBy, PageSortOrder} from "../Const";
|
||||||
|
|
||||||
export function FileList(props) {
|
export function FileList(props) {
|
||||||
const auth = useSelector((state) => state.login.jwt);
|
const auth = useSelector((state) => state.login.jwt);
|
||||||
|
const dispatch = useDispatch();
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
|
|
||||||
async function loadFileList() {
|
async function loadFileList() {
|
||||||
let req = await fetch("/admin/file", {
|
let pageReq = {
|
||||||
headers: {
|
page: 0,
|
||||||
"authorization": `Bearer ${auth}`
|
pageSize: 20,
|
||||||
}
|
sortBy: PagedSortBy.Date,
|
||||||
});
|
sortOrder: PageSortOrder.Dsc
|
||||||
|
};
|
||||||
|
let req = await AdminApi.fileList(auth, pageReq);
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
setFiles(await req.json());
|
setFiles(await req.json());
|
||||||
|
} else if (req.status === 401) {
|
||||||
|
dispatch(logout());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteFile(e, id) {
|
async function deleteFile(e, id) {
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
|
if (window.confirm(`Are you sure you want to delete: ${id}?`)) {
|
||||||
let req = await fetch(`/admin/file/${id}`, {
|
let req = await AdminApi.deleteFile(auth, id);
|
||||||
method: "DELETE",
|
if (req.ok) {
|
||||||
headers: {
|
setFiles([
|
||||||
"authorization": `Bearer ${auth}`
|
...files.filter(a => a.id !== id)
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
alert("Failed to delete file!");
|
||||||
}
|
}
|
||||||
});
|
|
||||||
if (req.ok) {
|
|
||||||
setFiles([
|
|
||||||
...files.filter(a => a.id !== id)
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
alert("Failed to delete file!");
|
|
||||||
}
|
}
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
}
|
}
|
||||||
|
32
VoidCat/spa/src/Api.js
Normal file
32
VoidCat/spa/src/Api.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
async function getJson(method, url, auth, body) {
|
||||||
|
let headers = {
|
||||||
|
"Accept": "application/json"
|
||||||
|
};
|
||||||
|
if (auth) {
|
||||||
|
headers["Authorization"] = `Bearer ${auth}`;
|
||||||
|
}
|
||||||
|
if (body) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdminApi = {
|
||||||
|
fileList: (auth, pageReq) => getJson("POST", "/admin/file", auth, pageReq),
|
||||||
|
deleteFile: (auth, id) => getJson("DELETE", `/admin/file/${id}`, auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Api = {
|
||||||
|
stats: () => getJson("GET", "/stats"),
|
||||||
|
fileInfo: (id) => getJson("GET", `/upload/${id}`),
|
||||||
|
setPaywallConfig: (id, cfg) => getJson("POST", `/upload/${id}/paywall`, undefined, cfg),
|
||||||
|
createOrder: (id) => getJson("GET", `/upload/${id}/paywall`),
|
||||||
|
getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`),
|
||||||
|
login: (username, password) => getJson("POST", `/auth/login`, undefined, {username, password}),
|
||||||
|
register: (username, password) => getJson("POST", `/auth/register`, undefined, {username, password})
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
render(<App />);
|
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
@ -47,4 +47,16 @@ export const PaywallOrderState = {
|
|||||||
Unpaid: 0,
|
Unpaid: 0,
|
||||||
Paid: 1,
|
Paid: 1,
|
||||||
Expired: 2
|
Expired: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PagedSortBy = {
|
||||||
|
Name: 0,
|
||||||
|
Date: 1,
|
||||||
|
Size: 2,
|
||||||
|
Id: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageSortOrder = {
|
||||||
|
Asc: 0,
|
||||||
|
Dsc: 1
|
||||||
}
|
}
|
@ -3,14 +3,14 @@
|
|||||||
export function Countdown(props) {
|
export function Countdown(props) {
|
||||||
const [time, setTime] = useState(0);
|
const [time, setTime] = useState(0);
|
||||||
const onEnded = props.onEnded;
|
const onEnded = props.onEnded;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let t = setInterval(() => {
|
let t = setInterval(() => {
|
||||||
let to = new Date(props.to).getTime();
|
let to = new Date(props.to).getTime();
|
||||||
let now = new Date().getTime();
|
let now = new Date().getTime();
|
||||||
let seconds = (to - now) / 1000.0;
|
let seconds = (to - now) / 1000.0;
|
||||||
setTime(Math.max(0, seconds));
|
setTime(Math.max(0, seconds));
|
||||||
if(seconds <= 0 && typeof onEnded === "function") {
|
if (seconds <= 0 && typeof onEnded === "function") {
|
||||||
onEnded();
|
onEnded();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
@ -18,7 +18,7 @@ export function Dropzone(props) {
|
|||||||
|
|
||||||
function renderUploads() {
|
function renderUploads() {
|
||||||
let fElm = [];
|
let fElm = [];
|
||||||
for(let f of files) {
|
for (let f of files) {
|
||||||
fElm.push(<FileUpload file={f} key={f.name}/>);
|
fElm.push(<FileUpload file={f} key={f.name}/>);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
|
|
||||||
import "./FileEdit.css";
|
|
||||||
import {StrikePaywallConfig} from "./StrikePaywallConfig";
|
import {StrikePaywallConfig} from "./StrikePaywallConfig";
|
||||||
import {NoPaywallConfig} from "./NoPaywallConfig";
|
import {NoPaywallConfig} from "./NoPaywallConfig";
|
||||||
|
import {Api} from "./Api";
|
||||||
|
import "./FileEdit.css";
|
||||||
|
|
||||||
export function FileEdit(props) {
|
export function FileEdit(props) {
|
||||||
const file = props.file;
|
const file = props.file;
|
||||||
@ -14,16 +15,10 @@ export function FileEdit(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig(cfg) {
|
async function saveConfig(cfg) {
|
||||||
let req = await fetch(`/upload/${file.id}/paywall`, {
|
let req = await Api.setPaywallConfig(file.id, cfg);
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(cfg),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return req.ok;
|
return req.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPaywallConfig() {
|
function renderPaywallConfig() {
|
||||||
switch (paywall) {
|
switch (paywall) {
|
||||||
case 0: {
|
case 0: {
|
||||||
@ -47,7 +42,7 @@ export function FileEdit(props) {
|
|||||||
<dt>Description:</dt>
|
<dt>Description:</dt>
|
||||||
<dd><input type="text" value={meta.description}/></dd>
|
<dd><input type="text" value={meta.description}/></dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>Paywall Config</h3>
|
<h3>Paywall Config</h3>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import {ConstName, FormatCurrency} from "./Util";
|
import {FormatCurrency} from "./Util";
|
||||||
import {PaywallCurrencies, PaywallServices} from "./Const";
|
import {PaywallServices} from "./Const";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {LightningPaywall} from "./LightningPaywall";
|
import {LightningPaywall} from "./LightningPaywall";
|
||||||
|
import {Api} from "./Api";
|
||||||
|
|
||||||
export function FilePaywall(props) {
|
export function FilePaywall(props) {
|
||||||
const file = props.file;
|
const file = props.file;
|
||||||
@ -13,7 +14,7 @@ export function FilePaywall(props) {
|
|||||||
|
|
||||||
async function fetchOrder(e) {
|
async function fetchOrder(e) {
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
let req = await fetch(`/upload/${file.id}/paywall`);
|
let req = await Api.createOrder(file.id);
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
setOrder(await req.json());
|
setOrder(await req.json());
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import {TextPreview} from "./TextPreview";
|
|||||||
import "./FilePreview.css";
|
import "./FilePreview.css";
|
||||||
import {FileEdit} from "./FileEdit";
|
import {FileEdit} from "./FileEdit";
|
||||||
import {FilePaywall} from "./FilePaywall";
|
import {FilePaywall} from "./FilePaywall";
|
||||||
|
import {Api} from "./Api";
|
||||||
|
|
||||||
export function FilePreview() {
|
export function FilePreview() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -13,7 +14,7 @@ export function FilePreview() {
|
|||||||
const [link, setLink] = useState("#");
|
const [link, setLink] = useState("#");
|
||||||
|
|
||||||
async function loadInfo() {
|
async function loadInfo() {
|
||||||
let req = await fetch(`/upload/${params.id}`);
|
let req = await Api.fileInfo(params.id);
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
let info = await req.json();
|
let info = await req.json();
|
||||||
setInfo(info);
|
setInfo(info);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
import "./FileUpload.css";
|
|
||||||
import {buf2hex, ConstName, FormatBytes} from "./Util";
|
import {buf2hex, ConstName, FormatBytes} from "./Util";
|
||||||
import {RateCalculator} from "./RateCalculator";
|
import {RateCalculator} from "./RateCalculator";
|
||||||
|
|
||||||
|
import "./FileUpload.css";
|
||||||
|
|
||||||
const UploadState = {
|
const UploadState = {
|
||||||
NotStarted: 0,
|
NotStarted: 0,
|
||||||
Starting: 1,
|
Starting: 1,
|
||||||
@ -167,11 +167,11 @@ export function FileUpload(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChallengeElement() {
|
function getChallengeElement() {
|
||||||
let elm = document.createElement("iframe");
|
let elm = document.createElement("iframe");
|
||||||
elm.contentWindow.document.write(challenge);
|
elm.contentWindow.document.write(challenge);
|
||||||
return <div dangerouslySetInnerHTML={{ __html: elm.outerHTML }}/>;
|
return <div dangerouslySetInnerHTML={{__html: elm.outerHTML}}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -3,12 +3,13 @@ import FeatherIcon from "feather-icons-react";
|
|||||||
import {FormatBytes} from "./Util";
|
import {FormatBytes} from "./Util";
|
||||||
|
|
||||||
import "./GlobalStats.css";
|
import "./GlobalStats.css";
|
||||||
|
import {Api} from "./Api";
|
||||||
|
|
||||||
export function GlobalStats(props) {
|
export function GlobalStats(props) {
|
||||||
let [stats, setStats] = useState();
|
let [stats, setStats] = useState();
|
||||||
|
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
let req = await fetch("/stats");
|
let req = await Api.stats();
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
setStats(await req.json());
|
setStats(await req.json());
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import QRCode from "qrcode.react";
|
import QRCode from "qrcode.react";
|
||||||
import {Countdown} from "./Countdown";
|
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
|
|
||||||
|
import {Countdown} from "./Countdown";
|
||||||
import {PaywallOrderState} from "./Const";
|
import {PaywallOrderState} from "./Const";
|
||||||
|
import {Api} from "./Api";
|
||||||
|
|
||||||
export function LightningPaywall(props) {
|
export function LightningPaywall(props) {
|
||||||
const file = props.file;
|
const file = props.file;
|
||||||
@ -16,7 +18,7 @@ export function LightningPaywall(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkStatus() {
|
async function checkStatus() {
|
||||||
let req = await fetch(`/upload/${file.id}/paywall/${order.id}`);
|
let req = await Api.getOrder(file.id, order.id);
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
let order = await req.json();
|
let order = await req.json();
|
||||||
|
|
||||||
|
@ -3,34 +3,28 @@ import {useDispatch} from "react-redux";
|
|||||||
import {setAuth} from "./LoginState";
|
import {setAuth} from "./LoginState";
|
||||||
|
|
||||||
import "./Login.css";
|
import "./Login.css";
|
||||||
|
import {Api} from "./Api";
|
||||||
|
|
||||||
export function Login(props) {
|
export function Login() {
|
||||||
const [username, setUsername] = useState();
|
const [username, setUsername] = useState();
|
||||||
const [password, setPassword] = useState();
|
const [password, setPassword] = useState();
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
async function login(e, url) {
|
async function login(e, fnLogin) {
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
let req = await fetch(`/auth/${url}`, {
|
let req = await fnLogin(username, password);
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
username, password
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
let rsp = await req.json();
|
let rsp = await req.json();
|
||||||
if(rsp.jwt) {
|
if (rsp.jwt) {
|
||||||
dispatch(setAuth(rsp.jwt));
|
dispatch(setAuth(rsp.jwt));
|
||||||
} else {
|
} else {
|
||||||
setError(rsp.error);
|
setError(rsp.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,12 +33,12 @@ export function Login(props) {
|
|||||||
<h2>Login</h2>
|
<h2>Login</h2>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Username:</dt>
|
<dt>Username:</dt>
|
||||||
<dd><input onChange={(e) => setUsername(e.target.value)}/></dd>
|
<dd><input onChange={(e) => setUsername(e.target.value)} placeholder="user@example.com"/></dd>
|
||||||
<dt>Password:</dt>
|
<dt>Password:</dt>
|
||||||
<dd><input type="password" onChange={(e) => setPassword(e.target.value)}/></dd>
|
<dd><input type="password" onChange={(e) => setPassword(e.target.value)}/></dd>
|
||||||
</dl>
|
</dl>
|
||||||
<button onClick={(e) => login(e, "login")}>Login</button>
|
<button onClick={(e) => login(e, Api.login)}>Login</button>
|
||||||
<button onClick={(e) => login(e, "register")}>Register</button>
|
<button onClick={(e) => login(e, Api.register)}>Register</button>
|
||||||
{error ? <div className="error-msg">{error}</div> : null}
|
{error ? <div className="error-msg">{error}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -19,5 +19,5 @@ export const LoginState = createSlice({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setAuth, logout } = LoginState.actions;
|
export const {setAuth, logout} = LoginState.actions;
|
||||||
export default LoginState.reducer;
|
export default LoginState.reducer;
|
@ -3,7 +3,7 @@ export class RateCalculator {
|
|||||||
this.reports = [];
|
this.reports = [];
|
||||||
this.lastLoaded = 0;
|
this.lastLoaded = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportProgress(amount) {
|
ReportProgress(amount) {
|
||||||
this.reports.push({
|
this.reports.push({
|
||||||
time: new Date().getTime(),
|
time: new Date().getTime(),
|
||||||
@ -18,17 +18,17 @@ export class RateCalculator {
|
|||||||
});
|
});
|
||||||
this.lastLoaded = loaded;
|
this.lastLoaded = loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
RateWindow(s) {
|
RateWindow(s) {
|
||||||
let total = 0.0;
|
let total = 0.0;
|
||||||
|
|
||||||
let windowStart = new Date().getTime() - (s * 1000);
|
let windowStart = new Date().getTime() - (s * 1000);
|
||||||
for(let r of this.reports) {
|
for (let r of this.reports) {
|
||||||
if(r.time >= windowStart) {
|
if (r.time >= windowStart) {
|
||||||
total += r.amount;
|
total += r.amount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return total / s;
|
return total / s;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,15 +6,15 @@ export function TextPreview(props) {
|
|||||||
|
|
||||||
async function getContent(link) {
|
async function getContent(link) {
|
||||||
let req = await fetch(link);
|
let req = await fetch(link);
|
||||||
if(req.ok) {
|
if (req.ok) {
|
||||||
setContent(await req.text());
|
setContent(await req.text());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getContent(props.link);
|
getContent(props.link);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<pre className="text-preview">{content}</pre>
|
<pre className="text-preview">{content}</pre>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import * as Const from "./Const";
|
import * as Const from "./Const";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats bytes into binary notation
|
* Formats bytes into binary notation
|
||||||
* @param {number} b - The value in bytes
|
* @param {number} b - The value in bytes
|
||||||
@ -31,8 +32,8 @@ export function buf2hex(buffer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ConstName(type, val) {
|
export function ConstName(type, val) {
|
||||||
for(let [k, v] of Object.entries(type)) {
|
for (let [k, v] of Object.entries(type)) {
|
||||||
if(v === val) {
|
if (v === val) {
|
||||||
return k;
|
return k;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,7 +53,7 @@ export function FormatCurrency(value, currency) {
|
|||||||
})}`;
|
})}`;
|
||||||
}
|
}
|
||||||
case 1:
|
case 1:
|
||||||
case "USD":{
|
case "USD": {
|
||||||
return value.toLocaleString(undefined, {
|
return value.toLocaleString(undefined, {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD"
|
currency: "USD"
|
||||||
|
Loading…
Reference in New Issue
Block a user