forked from Kieran/void.cat
Add profiles
This commit is contained in:
parent
b1f5ca88f8
commit
0a946d8f74
@ -34,7 +34,7 @@ public class AdminController : Controller
|
|||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("user")]
|
[Route("user")]
|
||||||
public async Task<RenderedResults<PublicVoidUser>> ListUsers([FromBody] PagedRequest request)
|
public async Task<RenderedResults<PrivateVoidUser>> ListUsers([FromBody] PagedRequest request)
|
||||||
{
|
{
|
||||||
var result = await _userStore.ListUsers(request);
|
var result = await _userStore.ListUsers(request);
|
||||||
return await result.GetResults();
|
return await result.GetResults();
|
||||||
|
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
using VoidCat.Model.Paywall;
|
using VoidCat.Model.Paywall;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
using VoidCat.Services.Files;
|
||||||
|
|
||||||
namespace VoidCat.Controllers;
|
namespace VoidCat.Controllers;
|
||||||
|
|
||||||
@ -10,13 +11,15 @@ namespace VoidCat.Controllers;
|
|||||||
public class DownloadController : Controller
|
public class DownloadController : Controller
|
||||||
{
|
{
|
||||||
private readonly IFileStore _storage;
|
private readonly IFileStore _storage;
|
||||||
|
private readonly IFileInfoManager _fileInfo;
|
||||||
private readonly IPaywallStore _paywall;
|
private readonly IPaywallStore _paywall;
|
||||||
private readonly ILogger<DownloadController> _logger;
|
private readonly ILogger<DownloadController> _logger;
|
||||||
|
|
||||||
public DownloadController(IFileStore storage, ILogger<DownloadController> logger, IPaywallStore paywall)
|
public DownloadController(IFileStore storage, ILogger<DownloadController> logger, IFileInfoManager fileInfo, IPaywallStore paywall)
|
||||||
{
|
{
|
||||||
_storage = storage;
|
_storage = storage;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_fileInfo = fileInfo;
|
||||||
_paywall = paywall;
|
_paywall = paywall;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +40,7 @@ public class DownloadController : Controller
|
|||||||
var voidFile = await SetupDownload(gid);
|
var voidFile = await SetupDownload(gid);
|
||||||
if (voidFile == default) return;
|
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!");
|
||||||
@ -49,10 +52,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,7 +78,7 @@ public class DownloadController : Controller
|
|||||||
|
|
||||||
private async Task<PublicVoidFile?> SetupDownload(Guid id)
|
private async Task<PublicVoidFile?> SetupDownload(Guid id)
|
||||||
{
|
{
|
||||||
var meta = await _storage.Get(id);
|
var meta = await _fileInfo.Get(id);
|
||||||
if (meta == null)
|
if (meta == null)
|
||||||
{
|
{
|
||||||
Response.StatusCode = 404;
|
Response.StatusCode = 404;
|
||||||
@ -88,7 +91,7 @@ public class DownloadController : Controller
|
|||||||
var orderId = Request.Headers.GetHeader("V-OrderId") ?? Request.Query["orderId"];
|
var orderId = Request.Headers.GetHeader("V-OrderId") ?? Request.Query["orderId"];
|
||||||
if (!await IsOrderPaid(orderId))
|
if (!await IsOrderPaid(orderId))
|
||||||
{
|
{
|
||||||
Response.StatusCode = (int) HttpStatusCode.PaymentRequired;
|
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,4 +142,4 @@ public class DownloadController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,14 +15,16 @@ namespace VoidCat.Controllers
|
|||||||
private readonly IFileMetadataStore _metadata;
|
private readonly IFileMetadataStore _metadata;
|
||||||
private readonly IPaywallStore _paywall;
|
private readonly IPaywallStore _paywall;
|
||||||
private readonly IPaywallFactory _paywallFactory;
|
private readonly IPaywallFactory _paywallFactory;
|
||||||
|
private readonly IFileInfoManager _fileInfo;
|
||||||
|
|
||||||
public UploadController(IFileStore storage, IFileMetadataStore metadata, IPaywallStore paywall,
|
public UploadController(IFileStore storage, IFileMetadataStore metadata, IPaywallStore paywall,
|
||||||
IPaywallFactory paywallFactory)
|
IPaywallFactory paywallFactory, IFileInfoManager fileInfo)
|
||||||
{
|
{
|
||||||
_storage = storage;
|
_storage = storage;
|
||||||
_metadata = metadata;
|
_metadata = metadata;
|
||||||
_paywall = paywall;
|
_paywall = paywall;
|
||||||
_paywallFactory = paywallFactory;
|
_paywallFactory = paywallFactory;
|
||||||
|
_fileInfo = fileInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@ -33,7 +35,7 @@ namespace VoidCat.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var uid = HttpContext.GetUserId();
|
var uid = HttpContext.GetUserId();
|
||||||
var meta = new VoidFileMeta()
|
var meta = new SecretVoidFileMeta()
|
||||||
{
|
{
|
||||||
MimeType = Request.Headers.GetHeader("V-Content-Type"),
|
MimeType = Request.Headers.GetHeader("V-Content-Type"),
|
||||||
Name = Request.Headers.GetHeader("V-Filename"),
|
Name = Request.Headers.GetHeader("V-Filename"),
|
||||||
@ -62,12 +64,12 @@ namespace VoidCat.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var gid = id.FromBase58Guid();
|
var gid = id.FromBase58Guid();
|
||||||
var fileInfo = await _storage.Get(gid);
|
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
||||||
if (fileInfo == default) return UploadResult.Error("File not found");
|
if (meta == default) return UploadResult.Error("File not found");
|
||||||
|
|
||||||
var editSecret = Request.Headers.GetHeader("V-EditSecret");
|
var editSecret = Request.Headers.GetHeader("V-EditSecret");
|
||||||
var digest = Request.Headers.GetHeader("V-Digest");
|
var digest = Request.Headers.GetHeader("V-Digest");
|
||||||
var vf = await _storage.Ingress(new(Request.Body, fileInfo.Metadata, digest!)
|
var vf = await _storage.Ingress(new(Request.Body, meta, digest!)
|
||||||
{
|
{
|
||||||
EditSecret = editSecret?.FromBase58Guid() ?? Guid.Empty,
|
EditSecret = editSecret?.FromBase58Guid() ?? Guid.Empty,
|
||||||
Id = gid
|
Id = gid
|
||||||
@ -85,7 +87,7 @@ namespace VoidCat.Controllers
|
|||||||
[Route("{id}")]
|
[Route("{id}")]
|
||||||
public ValueTask<PublicVoidFile?> GetInfo([FromRoute] string id)
|
public ValueTask<PublicVoidFile?> GetInfo([FromRoute] string id)
|
||||||
{
|
{
|
||||||
return _storage.Get(id.FromBase58Guid());
|
return _fileInfo.Get(id.FromBase58Guid());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@ -93,16 +95,16 @@ namespace VoidCat.Controllers
|
|||||||
public async ValueTask<PaywallOrder?> CreateOrder([FromRoute] string id)
|
public async ValueTask<PaywallOrder?> CreateOrder([FromRoute] string id)
|
||||||
{
|
{
|
||||||
var gid = id.FromBase58Guid();
|
var gid = id.FromBase58Guid();
|
||||||
var file = await _storage.Get(gid);
|
var file = await _fileInfo.Get(gid);
|
||||||
var config = await _paywall.GetConfig(gid);
|
var config = await _paywall.GetConfig(gid);
|
||||||
|
|
||||||
var provider = await _paywallFactory.CreateProvider(config!.Service);
|
var provider = await _paywallFactory.CreateProvider(config!.Service);
|
||||||
return await provider.CreateOrder(file!);
|
return await provider.CreateOrder(file!);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{id}/paywall/{order:guid}")]
|
[Route("{id}/paywall/{order:guid}")]
|
||||||
public async ValueTask<PaywallOrder?> GetOrderStatus([FromRoute] string id, [FromRoute]Guid order)
|
public async ValueTask<PaywallOrder?> GetOrderStatus([FromRoute] string id, [FromRoute] Guid order)
|
||||||
{
|
{
|
||||||
var gid = id.FromBase58Guid();
|
var gid = id.FromBase58Guid();
|
||||||
var config = await _paywall.GetConfig(gid);
|
var config = await _paywall.GetConfig(gid);
|
||||||
@ -116,7 +118,7 @@ namespace VoidCat.Controllers
|
|||||||
public async Task<IActionResult> SetPaywallConfig([FromRoute] string id, [FromBody] SetPaywallConfigRequest req)
|
public async Task<IActionResult> SetPaywallConfig([FromRoute] string id, [FromBody] SetPaywallConfigRequest req)
|
||||||
{
|
{
|
||||||
var gid = id.FromBase58Guid();
|
var gid = id.FromBase58Guid();
|
||||||
var meta = await _metadata.Get(gid);
|
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
||||||
if (meta == default) return NotFound();
|
if (meta == default) return NotFound();
|
||||||
|
|
||||||
if (req.EditSecret != meta.EditSecret) return Unauthorized();
|
if (req.EditSecret != meta.EditSecret) return Unauthorized();
|
||||||
@ -165,4 +167,4 @@ namespace VoidCat.Controllers
|
|||||||
|
|
||||||
public StrikePaywallConfig? Strike { get; init; }
|
public StrikePaywallConfig? Strike { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
VoidCat/Controllers/UserController.cs
Normal file
32
VoidCat/Controllers/UserController.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Controllers;
|
||||||
|
|
||||||
|
[Route("user")]
|
||||||
|
public class UserController : Controller
|
||||||
|
{
|
||||||
|
private readonly IUserStore _store;
|
||||||
|
|
||||||
|
public UserController(IUserStore store)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{id}")]
|
||||||
|
public async Task<VoidUser?> GetUser([FromRoute] string id)
|
||||||
|
{
|
||||||
|
var loggedUser = HttpContext.GetUserId();
|
||||||
|
var requestedId = id.FromBase58Guid();
|
||||||
|
if (loggedUser == requestedId)
|
||||||
|
{
|
||||||
|
return await _store.Get<PrivateVoidUser>(id.FromBase58Guid());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return await _store.Get<PublicVoidUser>(id.FromBase58Guid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -155,7 +155,7 @@ public static class Extensions
|
|||||||
throw new ArgumentException("Unknown algo", nameof(algo));
|
throw new ArgumentException("Unknown algo", nameof(algo));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool CheckPassword(this PrivateVoidUser vu, string password)
|
public static bool CheckPassword(this InternalVoidUser vu, string password)
|
||||||
{
|
{
|
||||||
var hashParts = vu.PasswordHash.Split(":");
|
var hashParts = vu.PasswordHash.Split(":");
|
||||||
return vu.PasswordHash == password.HashPassword(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
|
return vu.PasswordHash == password.HashPassword(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
namespace VoidCat.Model;
|
namespace VoidCat.Model;
|
||||||
|
|
||||||
public sealed record IngressPayload(Stream InStream, VoidFileMeta Meta, string Hash)
|
public sealed record IngressPayload(Stream InStream, SecretVoidFileMeta Meta, string Hash)
|
||||||
{
|
{
|
||||||
public Guid? Id { get; init; }
|
public Guid? Id { get; init; }
|
||||||
public Guid? EditSecret { get; init; }
|
public Guid? EditSecret { get; init; }
|
||||||
|
@ -20,15 +20,23 @@ namespace VoidCat.Model
|
|||||||
/// Optional paywall config
|
/// Optional paywall config
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PaywallConfig? Paywall { get; init; }
|
public PaywallConfig? Paywall { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User profile that uploaded the file
|
||||||
|
/// </summary>
|
||||||
|
public PublicVoidUser? Uploader { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Traffic stats for this file
|
||||||
|
/// </summary>
|
||||||
|
public Bandwidth? Bandwidth { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record PublicVoidFile : VoidFile<VoidFileMeta>
|
public sealed record PublicVoidFile : VoidFile<VoidFileMeta>
|
||||||
{
|
{
|
||||||
public Bandwidth? Bandwidth { get; init; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record PrivateVoidFile : VoidFile<SecretVoidFileMeta>
|
public sealed record PrivateVoidFile : VoidFile<SecretVoidFileMeta>
|
||||||
{
|
{
|
||||||
public Bandwidth? Bandwidth { get; init; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,32 +1,41 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using VoidCat.Model;
|
|
||||||
|
|
||||||
namespace VoidCat.Model;
|
namespace VoidCat.Model;
|
||||||
|
|
||||||
public abstract class VoidUser
|
public abstract class VoidUser
|
||||||
{
|
{
|
||||||
protected VoidUser(Guid id, string email)
|
protected VoidUser(Guid id)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Email = email;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConverter(typeof(Base58GuidConverter))]
|
[JsonConverter(typeof(Base58GuidConverter))]
|
||||||
public Guid Id { get; }
|
public Guid Id { get; }
|
||||||
|
|
||||||
public string Email { get; }
|
public HashSet<string> Roles { get; init; } = new() {Model.Roles.User};
|
||||||
|
|
||||||
public HashSet<string> Roles { get; init; } = new() { Model.Roles.User };
|
|
||||||
|
|
||||||
public DateTimeOffset Created { get; init; }
|
public DateTimeOffset Created { get; init; }
|
||||||
|
|
||||||
public DateTimeOffset LastLogin { get; set; }
|
public DateTimeOffset LastLogin { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display avatar for user profile
|
||||||
|
/// </summary>
|
||||||
public string? Avatar { get; set; }
|
public string? Avatar { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display name for user profile
|
||||||
|
/// </summary>
|
||||||
|
public string? DisplayName { get; set; } = "void user";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Public profile, otherwise the profile will be hidden
|
||||||
|
/// </summary>
|
||||||
|
public bool Public { get; set; } = true;
|
||||||
|
|
||||||
public PublicVoidUser ToPublic()
|
public PublicVoidUser ToPublic()
|
||||||
{
|
{
|
||||||
return new(Id, Email)
|
return new(Id)
|
||||||
{
|
{
|
||||||
Roles = Roles,
|
Roles = Roles,
|
||||||
Created = Created,
|
Created = Created,
|
||||||
@ -36,9 +45,9 @@ public abstract class VoidUser
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PrivateVoidUser : VoidUser
|
public sealed class InternalVoidUser : PrivateVoidUser
|
||||||
{
|
{
|
||||||
public PrivateVoidUser(Guid id, string email, string passwordHash) : base(id, email)
|
public InternalVoidUser(Guid id, string email, string passwordHash) : base(id, email)
|
||||||
{
|
{
|
||||||
PasswordHash = passwordHash;
|
PasswordHash = passwordHash;
|
||||||
}
|
}
|
||||||
@ -46,9 +55,19 @@ public sealed class PrivateVoidUser : VoidUser
|
|||||||
public string PasswordHash { get; }
|
public string PasswordHash { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PrivateVoidUser : VoidUser
|
||||||
|
{
|
||||||
|
public PrivateVoidUser(Guid id, string email) : base(id)
|
||||||
|
{
|
||||||
|
Email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Email { get; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class PublicVoidUser : VoidUser
|
public sealed class PublicVoidUser : VoidUser
|
||||||
{
|
{
|
||||||
public PublicVoidUser(Guid id, string email) : base(id, email)
|
public PublicVoidUser(Guid id) : base(id)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,17 @@ using VoidCat.Services.Redis;
|
|||||||
using VoidCat.Services.Stats;
|
using VoidCat.Services.Stats;
|
||||||
using VoidCat.Services.Users;
|
using VoidCat.Services.Users;
|
||||||
|
|
||||||
|
// setup JsonConvert default settings
|
||||||
|
JsonSerializerSettings ConfigJsonSettings(JsonSerializerSettings s)
|
||||||
|
{
|
||||||
|
s.NullValueHandling = NullValueHandling.Ignore;
|
||||||
|
s.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
|
||||||
|
s.MissingMemberHandling = MissingMemberHandling.Ignore;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonConvert.DefaultSettings = () => ConfigJsonSettings(new());
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
var services = builder.Services;
|
var services = builder.Services;
|
||||||
|
|
||||||
@ -44,12 +55,7 @@ services.AddCors(opt =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
services.AddControllers().AddNewtonsoftJson((opt) =>
|
services.AddControllers().AddNewtonsoftJson((opt) => { ConfigJsonSettings(opt.SerializerSettings); });
|
||||||
{
|
|
||||||
opt.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
|
||||||
opt.SerializerSettings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
|
|
||||||
opt.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.AddJwtBearer(options =>
|
.AddJwtBearer(options =>
|
||||||
@ -65,13 +71,7 @@ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddAuthorization((opt) =>
|
services.AddAuthorization((opt) => { opt.AddPolicy(Policies.RequireAdmin, (auth) => { auth.RequireRole(Roles.Admin); }); });
|
||||||
{
|
|
||||||
opt.AddPolicy(Policies.RequireAdmin, (auth) =>
|
|
||||||
{
|
|
||||||
auth.RequireRole(Roles.Admin);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// void.cat services
|
// void.cat services
|
||||||
//
|
//
|
||||||
@ -80,6 +80,7 @@ services.AddVoidMigrations();
|
|||||||
// file storage
|
// file storage
|
||||||
services.AddTransient<IFileMetadataStore, LocalDiskFileMetadataStore>();
|
services.AddTransient<IFileMetadataStore, LocalDiskFileMetadataStore>();
|
||||||
services.AddTransient<IFileStore, LocalDiskFileStore>();
|
services.AddTransient<IFileStore, LocalDiskFileStore>();
|
||||||
|
services.AddTransient<IFileInfoManager, FileInfoManager>();
|
||||||
|
|
||||||
// stats
|
// stats
|
||||||
services.AddTransient<IAggregateStatsCollector, AggregateStatsCollector>();
|
services.AddTransient<IAggregateStatsCollector, AggregateStatsCollector>();
|
||||||
|
8
VoidCat/Services/Abstractions/IFileInfoManager.cs
Normal file
8
VoidCat/Services/Abstractions/IFileInfoManager.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using VoidCat.Model;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
public interface IFileInfoManager
|
||||||
|
{
|
||||||
|
ValueTask<PublicVoidFile?> Get(Guid id);
|
||||||
|
}
|
@ -4,8 +4,7 @@ namespace VoidCat.Services.Abstractions;
|
|||||||
|
|
||||||
public interface IFileMetadataStore
|
public interface IFileMetadataStore
|
||||||
{
|
{
|
||||||
ValueTask<SecretVoidFileMeta?> Get(Guid id);
|
ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta;
|
||||||
ValueTask<VoidFileMeta?> GetPublic(Guid id);
|
|
||||||
|
|
||||||
ValueTask Set(Guid id, SecretVoidFileMeta meta);
|
ValueTask Set(Guid id, SecretVoidFileMeta meta);
|
||||||
|
|
||||||
|
@ -4,8 +4,6 @@ namespace VoidCat.Services.Abstractions;
|
|||||||
|
|
||||||
public interface IFileStore
|
public interface IFileStore
|
||||||
{
|
{
|
||||||
ValueTask<PublicVoidFile?> Get(Guid id);
|
|
||||||
|
|
||||||
ValueTask<PrivateVoidFile> Ingress(IngressPayload payload, CancellationToken cts);
|
ValueTask<PrivateVoidFile> Ingress(IngressPayload payload, CancellationToken cts);
|
||||||
|
|
||||||
ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts);
|
ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts);
|
||||||
|
@ -6,6 +6,6 @@ public interface IUserStore
|
|||||||
{
|
{
|
||||||
ValueTask<Guid?> LookupUser(string email);
|
ValueTask<Guid?> LookupUser(string email);
|
||||||
ValueTask<T?> Get<T>(Guid id) where T : VoidUser;
|
ValueTask<T?> Get<T>(Guid id) where T : VoidUser;
|
||||||
ValueTask Set(PrivateVoidUser user);
|
ValueTask Set(InternalVoidUser user);
|
||||||
ValueTask<PagedResult<PublicVoidUser>> ListUsers(PagedRequest request);
|
ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request);
|
||||||
}
|
}
|
43
VoidCat/Services/Files/FileInfoManager.cs
Normal file
43
VoidCat/Services/Files/FileInfoManager.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Files;
|
||||||
|
|
||||||
|
public class FileInfoManager : IFileInfoManager
|
||||||
|
{
|
||||||
|
private readonly IFileMetadataStore _metadataStore;
|
||||||
|
private readonly IPaywallStore _paywallStore;
|
||||||
|
private readonly IStatsReporter _statsReporter;
|
||||||
|
private readonly IUserStore _userStore;
|
||||||
|
|
||||||
|
public FileInfoManager(IFileMetadataStore metadataStore, IPaywallStore paywallStore, IStatsReporter statsReporter,
|
||||||
|
IUserStore userStore)
|
||||||
|
{
|
||||||
|
_metadataStore = metadataStore;
|
||||||
|
_paywallStore = paywallStore;
|
||||||
|
_statsReporter = statsReporter;
|
||||||
|
_userStore = userStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<PublicVoidFile?> Get(Guid id)
|
||||||
|
{
|
||||||
|
var meta = _metadataStore.Get<VoidFileMeta>(id);
|
||||||
|
var paywall = _paywallStore.GetConfig(id);
|
||||||
|
var bandwidth = _statsReporter.GetBandwidth(id);
|
||||||
|
await Task.WhenAll(meta.AsTask(), paywall.AsTask(), bandwidth.AsTask());
|
||||||
|
|
||||||
|
if (meta.Result == default) return default;
|
||||||
|
|
||||||
|
var uploader = meta.Result?.Uploader;
|
||||||
|
var user = uploader.HasValue ? await _userStore.Get<PublicVoidUser>(uploader.Value) : null;
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Metadata = meta.Result,
|
||||||
|
Paywall = paywall.Result,
|
||||||
|
Bandwidth = bandwidth.Result,
|
||||||
|
Uploader = user?.Public == true ? user : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -23,14 +23,9 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<VoidFileMeta?> GetPublic(Guid id)
|
public ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta
|
||||||
{
|
{
|
||||||
return GetMeta<VoidFileMeta>(id);
|
return GetMeta<TMeta>(id);
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask<SecretVoidFileMeta?> Get(Guid id)
|
|
||||||
{
|
|
||||||
return GetMeta<SecretVoidFileMeta>(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask Set(Guid id, SecretVoidFileMeta meta)
|
public async ValueTask Set(Guid id, SecretVoidFileMeta meta)
|
||||||
@ -42,7 +37,7 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
|
|||||||
|
|
||||||
public async ValueTask Update(Guid id, SecretVoidFileMeta patch)
|
public async ValueTask Update(Guid id, SecretVoidFileMeta patch)
|
||||||
{
|
{
|
||||||
var oldMeta = await Get(id);
|
var oldMeta = await Get<SecretVoidFileMeta>(id);
|
||||||
if (oldMeta?.EditSecret != patch.EditSecret)
|
if (oldMeta?.EditSecret != patch.EditSecret)
|
||||||
{
|
{
|
||||||
throw new VoidNotAllowedException("Edit secret incorrect");
|
throw new VoidNotAllowedException("Edit secret incorrect");
|
||||||
@ -74,4 +69,4 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
|
|||||||
|
|
||||||
private string MapMeta(Guid id) =>
|
private string MapMeta(Guid id) =>
|
||||||
Path.ChangeExtension(Path.Join(_settings.DataDirectory, MetadataDir, id.ToString()), ".json");
|
Path.ChangeExtension(Path.Join(_settings.DataDirectory, MetadataDir, id.ToString()), ".json");
|
||||||
}
|
}
|
||||||
|
@ -32,22 +32,6 @@ public class LocalDiskFileStore : IFileStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
Metadata = meta.Result,
|
|
||||||
Paywall = paywall.Result,
|
|
||||||
Bandwidth = bandwidth.Result
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts)
|
public async ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts)
|
||||||
{
|
{
|
||||||
var path = MapPath(request.Id);
|
var path = MapPath(request.Id);
|
||||||
@ -68,10 +52,9 @@ public class LocalDiskFileStore : IFileStore
|
|||||||
{
|
{
|
||||||
var id = payload.Id ?? Guid.NewGuid();
|
var id = payload.Id ?? Guid.NewGuid();
|
||||||
var fPath = MapPath(id);
|
var fPath = MapPath(id);
|
||||||
SecretVoidFileMeta? vf = null;
|
var vf = payload.Meta;
|
||||||
if (payload.IsAppend)
|
if (payload.IsAppend)
|
||||||
{
|
{
|
||||||
vf = await _metadataStore.Get(payload.Id!.Value);
|
|
||||||
if (vf?.EditSecret != null && vf.EditSecret != payload.EditSecret)
|
if (vf?.EditSecret != null && vf.EditSecret != payload.EditSecret)
|
||||||
{
|
{
|
||||||
throw new VoidNotAllowedException("Edit secret incorrect!");
|
throw new VoidNotAllowedException("Edit secret incorrect!");
|
||||||
@ -98,13 +81,8 @@ public class LocalDiskFileStore : IFileStore
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
vf = new SecretVoidFileMeta()
|
vf = vf! with
|
||||||
{
|
{
|
||||||
Name = payload.Meta.Name,
|
|
||||||
Description = payload.Meta.Description,
|
|
||||||
Digest = payload.Meta.Digest,
|
|
||||||
MimeType = payload.Meta.MimeType,
|
|
||||||
Uploader = payload.Meta.Uploader,
|
|
||||||
Uploaded = DateTimeOffset.UtcNow,
|
Uploaded = DateTimeOffset.UtcNow,
|
||||||
EditSecret = Guid.NewGuid(),
|
EditSecret = Guid.NewGuid(),
|
||||||
Size = total
|
Size = total
|
||||||
@ -146,10 +124,14 @@ public class LocalDiskFileStore : IFileStore
|
|||||||
{
|
{
|
||||||
if (!Guid.TryParse(Path.GetFileNameWithoutExtension(file), out var gid)) continue;
|
if (!Guid.TryParse(Path.GetFileNameWithoutExtension(file), out var gid)) continue;
|
||||||
|
|
||||||
var loaded = await Get(gid);
|
var loaded = await _metadataStore.Get<VoidFileMeta>(gid);
|
||||||
if (loaded != default)
|
if (loaded != default)
|
||||||
{
|
{
|
||||||
yield return loaded;
|
yield return new()
|
||||||
|
{
|
||||||
|
Id = gid,
|
||||||
|
Metadata = loaded
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ public class UserManager : IUserManager
|
|||||||
var userId = await _store.LookupUser(email);
|
var userId = await _store.LookupUser(email);
|
||||||
if (!userId.HasValue) throw new InvalidOperationException("User does not exist");
|
if (!userId.HasValue) throw new InvalidOperationException("User does not exist");
|
||||||
|
|
||||||
var user = await _store.Get<PrivateVoidUser>(userId.Value);
|
var user = await _store.Get<InternalVoidUser>(userId.Value);
|
||||||
if (!(user?.CheckPassword(password) ?? false)) throw new InvalidOperationException("User does not exist");
|
if (!(user?.CheckPassword(password) ?? false)) throw new InvalidOperationException("User does not exist");
|
||||||
|
|
||||||
user.LastLogin = DateTimeOffset.UtcNow;
|
user.LastLogin = DateTimeOffset.UtcNow;
|
||||||
@ -32,7 +32,7 @@ public class UserManager : IUserManager
|
|||||||
var existingUser = await _store.LookupUser(email);
|
var existingUser = await _store.LookupUser(email);
|
||||||
if (existingUser != Guid.Empty) throw new InvalidOperationException("User already exists");
|
if (existingUser != Guid.Empty) throw new InvalidOperationException("User already exists");
|
||||||
|
|
||||||
var newUser = new PrivateVoidUser(Guid.NewGuid(), email, password.HashPassword())
|
var newUser = new InternalVoidUser(Guid.NewGuid(), email, password.HashPassword())
|
||||||
{
|
{
|
||||||
Created = DateTimeOffset.UtcNow,
|
Created = DateTimeOffset.UtcNow,
|
||||||
LastLogin = DateTimeOffset.UtcNow
|
LastLogin = DateTimeOffset.UtcNow
|
||||||
|
@ -23,14 +23,14 @@ public class UserStore : IUserStore
|
|||||||
return await _cache.Get<T>(MapKey(id));
|
return await _cache.Get<T>(MapKey(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask Set(PrivateVoidUser user)
|
public async ValueTask Set(InternalVoidUser user)
|
||||||
{
|
{
|
||||||
await _cache.Set(MapKey(user.Id), user);
|
await _cache.Set(MapKey(user.Id), user);
|
||||||
await _cache.AddToList(UserList, user.Id.ToString());
|
await _cache.AddToList(UserList, user.Id.ToString());
|
||||||
await _cache.Set(MapKey(user.Email), user.Id.ToString());
|
await _cache.Set(MapKey(user.Email), user.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<PagedResult<PublicVoidUser>> ListUsers(PagedRequest request)
|
public async ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request)
|
||||||
{
|
{
|
||||||
var users = (await _cache.GetList(UserList))?.Select(Guid.Parse);
|
var users = (await _cache.GetList(UserList))?.Select(Guid.Parse);
|
||||||
users = (request.SortBy, request.SortOrder) switch
|
users = (request.SortBy, request.SortOrder) switch
|
||||||
@ -40,9 +40,9 @@ public class UserStore : IUserStore
|
|||||||
_ => users
|
_ => users
|
||||||
};
|
};
|
||||||
|
|
||||||
async IAsyncEnumerable<PublicVoidUser> EnumerateUsers(IEnumerable<Guid> ids)
|
async IAsyncEnumerable<PrivateVoidUser> EnumerateUsers(IEnumerable<Guid> ids)
|
||||||
{
|
{
|
||||||
var usersLoaded = await Task.WhenAll(ids.Select(async a => await Get<PublicVoidUser>(a)));
|
var usersLoaded = await Task.WhenAll(ids.Select(async a => await Get<PrivateVoidUser>(a)));
|
||||||
foreach (var user in usersLoaded)
|
foreach (var user in usersLoaded)
|
||||||
{
|
{
|
||||||
if (user != default)
|
if (user != default)
|
||||||
|
@ -36,7 +36,8 @@ export function useApi() {
|
|||||||
createOrder: (id) => getJson("GET", `/upload/${id}/paywall`),
|
createOrder: (id) => getJson("GET", `/upload/${id}/paywall`),
|
||||||
getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`),
|
getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`),
|
||||||
login: (username, password) => getJson("POST", `/auth/login`, {username, password}),
|
login: (username, password) => getJson("POST", `/auth/login`, {username, password}),
|
||||||
register: (username, password) => getJson("POST", `/auth/register`, {username, password})
|
register: (username, password) => getJson("POST", `/auth/register`, {username, password}),
|
||||||
|
getUser: (id) => getJson("GET", `/user/${id}`, undefined, auth)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -2,6 +2,8 @@ import preval from "preval.macro";
|
|||||||
|
|
||||||
export const ApiHost = preval`module.exports = process.env.API_HOST || '';`;
|
export const ApiHost = preval`module.exports = process.env.API_HOST || '';`;
|
||||||
|
|
||||||
|
export const DefaultAvatar = "https://i.imgur.com/8A5Fu65.jpeg";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @constant {number} - Size of 1 kiB
|
* @constant {number} - Size of 1 kiB
|
||||||
*/
|
*/
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
.preview {
|
.preview {
|
||||||
text-align: center;
|
|
||||||
margin-top: 2vh;
|
margin-top: 2vh;
|
||||||
width: 720px;
|
width: 720px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview > a {
|
.preview > a {
|
||||||
|
@ -9,6 +9,7 @@ import {useApi} from "./Api";
|
|||||||
import {Helmet} from "react-helmet";
|
import {Helmet} from "react-helmet";
|
||||||
import {FormatBytes} from "./Util";
|
import {FormatBytes} from "./Util";
|
||||||
import {ApiHost} from "./Const";
|
import {ApiHost} from "./Const";
|
||||||
|
import {FileUploader} from "./FileUploader";
|
||||||
|
|
||||||
export function FilePreview() {
|
export function FilePreview() {
|
||||||
const {Api} = useApi();
|
const {Api} = useApi();
|
||||||
@ -121,6 +122,7 @@ export function FilePreview() {
|
|||||||
{FormatBytes(info?.metadata?.size ?? 0, 2)}
|
{FormatBytes(info?.metadata?.size ?? 0, 2)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{info.uploader ? <FileUploader uploader={info.uploader}/> : null}
|
||||||
<FileEdit file={info}/>
|
<FileEdit file={info}/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : "Not Found"}
|
) : "Not Found"}
|
||||||
|
22
VoidCat/spa/src/FileUploader.css
Normal file
22
VoidCat/spa/src/FileUploader.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.uploader-info {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-info .small-profile {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-info .small-profile .avatar {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-info .small-profile .name {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
22
VoidCat/spa/src/FileUploader.js
Normal file
22
VoidCat/spa/src/FileUploader.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {DefaultAvatar} from "./Const";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
import "./FileUploader.css";
|
||||||
|
|
||||||
|
export function FileUploader(props) {
|
||||||
|
const uploader = props.uploader;
|
||||||
|
|
||||||
|
let avatarStyles = {
|
||||||
|
backgroundImage: `url(${uploader.avatar ?? DefaultAvatar})`
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="uploader-info">
|
||||||
|
<Link to={`/u/${uploader.id}`}>
|
||||||
|
<div className="small-profile">
|
||||||
|
<div className="avatar" style={avatarStyles}/>
|
||||||
|
<div className="name">{uploader.displayName}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
12
VoidCat/spa/src/Profile.css
Normal file
12
VoidCat/spa/src/Profile.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.profile {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .avatar {
|
||||||
|
width: 256px;
|
||||||
|
height: 256px;
|
||||||
|
border-radius: 40px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
@ -1,5 +1,47 @@
|
|||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {useParams} from "react-router-dom";
|
||||||
|
import {useApi} from "./Api";
|
||||||
|
import {DefaultAvatar} from "./Const";
|
||||||
|
|
||||||
|
import "./Profile.css";
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
return (
|
const [profile, setProfile] = useState();
|
||||||
<h1>Coming soon..</h1>
|
const {Api} = useApi();
|
||||||
);
|
const params = useParams();
|
||||||
|
|
||||||
|
async function loadProfile() {
|
||||||
|
let p = await Api.getUser(params.id);
|
||||||
|
if (p.ok) {
|
||||||
|
setProfile(await p.json());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadProfile();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
let avatarStyles = {
|
||||||
|
backgroundImage: `url(${profile.avatar ?? DefaultAvatar})`
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<div className="profile">
|
||||||
|
<h2>{profile.displayName}</h2>
|
||||||
|
<div className="avatar" style={avatarStyles}/>
|
||||||
|
<div className="roles">
|
||||||
|
<h3>Roles:</h3>
|
||||||
|
{profile.roles.map(a => <span className="btn">{a}</span>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<h1>Loading..</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user