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]
|
||||
[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);
|
||||
return await result.GetResults();
|
||||
|
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Model.Paywall;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using VoidCat.Services.Files;
|
||||
|
||||
namespace VoidCat.Controllers;
|
||||
|
||||
@ -10,13 +11,15 @@ namespace VoidCat.Controllers;
|
||||
public class DownloadController : Controller
|
||||
{
|
||||
private readonly IFileStore _storage;
|
||||
private readonly IFileInfoManager _fileInfo;
|
||||
private readonly IPaywallStore _paywall;
|
||||
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;
|
||||
_logger = logger;
|
||||
_fileInfo = fileInfo;
|
||||
_paywall = paywall;
|
||||
}
|
||||
|
||||
@ -37,7 +40,7 @@ public class DownloadController : Controller
|
||||
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!");
|
||||
@ -49,10 +52,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;
|
||||
}
|
||||
}
|
||||
@ -75,7 +78,7 @@ public class DownloadController : Controller
|
||||
|
||||
private async Task<PublicVoidFile?> SetupDownload(Guid id)
|
||||
{
|
||||
var meta = await _storage.Get(id);
|
||||
var meta = await _fileInfo.Get(id);
|
||||
if (meta == null)
|
||||
{
|
||||
Response.StatusCode = 404;
|
||||
@ -88,7 +91,7 @@ public class DownloadController : Controller
|
||||
var orderId = Request.Headers.GetHeader("V-OrderId") ?? Request.Query["orderId"];
|
||||
if (!await IsOrderPaid(orderId))
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.PaymentRequired;
|
||||
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@ -139,4 +142,4 @@ public class DownloadController : Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,14 +15,16 @@ namespace VoidCat.Controllers
|
||||
private readonly IFileMetadataStore _metadata;
|
||||
private readonly IPaywallStore _paywall;
|
||||
private readonly IPaywallFactory _paywallFactory;
|
||||
private readonly IFileInfoManager _fileInfo;
|
||||
|
||||
public UploadController(IFileStore storage, IFileMetadataStore metadata, IPaywallStore paywall,
|
||||
IPaywallFactory paywallFactory)
|
||||
IPaywallFactory paywallFactory, IFileInfoManager fileInfo)
|
||||
{
|
||||
_storage = storage;
|
||||
_metadata = metadata;
|
||||
_paywall = paywall;
|
||||
_paywallFactory = paywallFactory;
|
||||
_fileInfo = fileInfo;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -33,7 +35,7 @@ namespace VoidCat.Controllers
|
||||
try
|
||||
{
|
||||
var uid = HttpContext.GetUserId();
|
||||
var meta = new VoidFileMeta()
|
||||
var meta = new SecretVoidFileMeta()
|
||||
{
|
||||
MimeType = Request.Headers.GetHeader("V-Content-Type"),
|
||||
Name = Request.Headers.GetHeader("V-Filename"),
|
||||
@ -62,12 +64,12 @@ namespace VoidCat.Controllers
|
||||
try
|
||||
{
|
||||
var gid = id.FromBase58Guid();
|
||||
var fileInfo = await _storage.Get(gid);
|
||||
if (fileInfo == default) return UploadResult.Error("File not found");
|
||||
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
||||
if (meta == default) return UploadResult.Error("File not found");
|
||||
|
||||
var editSecret = Request.Headers.GetHeader("V-EditSecret");
|
||||
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,
|
||||
Id = gid
|
||||
@ -85,7 +87,7 @@ namespace VoidCat.Controllers
|
||||
[Route("{id}")]
|
||||
public ValueTask<PublicVoidFile?> GetInfo([FromRoute] string id)
|
||||
{
|
||||
return _storage.Get(id.FromBase58Guid());
|
||||
return _fileInfo.Get(id.FromBase58Guid());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -93,16 +95,16 @@ namespace VoidCat.Controllers
|
||||
public async ValueTask<PaywallOrder?> CreateOrder([FromRoute] string id)
|
||||
{
|
||||
var gid = id.FromBase58Guid();
|
||||
var file = await _storage.Get(gid);
|
||||
var file = await _fileInfo.Get(gid);
|
||||
var config = await _paywall.GetConfig(gid);
|
||||
|
||||
var provider = await _paywallFactory.CreateProvider(config!.Service);
|
||||
return await provider.CreateOrder(file!);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[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 config = await _paywall.GetConfig(gid);
|
||||
@ -116,7 +118,7 @@ namespace VoidCat.Controllers
|
||||
public async Task<IActionResult> SetPaywallConfig([FromRoute] string id, [FromBody] SetPaywallConfigRequest req)
|
||||
{
|
||||
var gid = id.FromBase58Guid();
|
||||
var meta = await _metadata.Get(gid);
|
||||
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
||||
if (meta == default) return NotFound();
|
||||
|
||||
if (req.EditSecret != meta.EditSecret) return Unauthorized();
|
||||
@ -165,4 +167,4 @@ namespace VoidCat.Controllers
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
public static bool CheckPassword(this PrivateVoidUser vu, string password)
|
||||
public static bool CheckPassword(this InternalVoidUser vu, string password)
|
||||
{
|
||||
var hashParts = vu.PasswordHash.Split(":");
|
||||
return vu.PasswordHash == password.HashPassword(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
|
||||
|
@ -1,6 +1,6 @@
|
||||
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? EditSecret { get; init; }
|
||||
|
@ -20,15 +20,23 @@ namespace VoidCat.Model
|
||||
/// Optional paywall config
|
||||
/// </summary>
|
||||
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 Bandwidth? Bandwidth { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PrivateVoidFile : VoidFile<SecretVoidFileMeta>
|
||||
{
|
||||
public Bandwidth? Bandwidth { get; init; }
|
||||
}
|
||||
}
|
@ -1,32 +1,41 @@
|
||||
using Newtonsoft.Json;
|
||||
using VoidCat.Model;
|
||||
|
||||
namespace VoidCat.Model;
|
||||
|
||||
public abstract class VoidUser
|
||||
{
|
||||
protected VoidUser(Guid id, string email)
|
||||
protected VoidUser(Guid id)
|
||||
{
|
||||
Id = id;
|
||||
Email = email;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(Base58GuidConverter))]
|
||||
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 LastLogin { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Display avatar for user profile
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
return new(Id, Email)
|
||||
return new(Id)
|
||||
{
|
||||
Roles = Roles,
|
||||
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;
|
||||
}
|
||||
@ -46,9 +55,19 @@ public sealed class PrivateVoidUser : VoidUser
|
||||
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 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.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 services = builder.Services;
|
||||
|
||||
@ -44,12 +55,7 @@ services.AddCors(opt =>
|
||||
});
|
||||
|
||||
services.AddRouting();
|
||||
services.AddControllers().AddNewtonsoftJson((opt) =>
|
||||
{
|
||||
opt.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
opt.SerializerSettings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
|
||||
opt.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
|
||||
});
|
||||
services.AddControllers().AddNewtonsoftJson((opt) => { ConfigJsonSettings(opt.SerializerSettings); });
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
@ -65,13 +71,7 @@ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthorization((opt) =>
|
||||
{
|
||||
opt.AddPolicy(Policies.RequireAdmin, (auth) =>
|
||||
{
|
||||
auth.RequireRole(Roles.Admin);
|
||||
});
|
||||
});
|
||||
services.AddAuthorization((opt) => { opt.AddPolicy(Policies.RequireAdmin, (auth) => { auth.RequireRole(Roles.Admin); }); });
|
||||
|
||||
// void.cat services
|
||||
//
|
||||
@ -80,6 +80,7 @@ services.AddVoidMigrations();
|
||||
// file storage
|
||||
services.AddTransient<IFileMetadataStore, LocalDiskFileMetadataStore>();
|
||||
services.AddTransient<IFileStore, LocalDiskFileStore>();
|
||||
services.AddTransient<IFileInfoManager, FileInfoManager>();
|
||||
|
||||
// stats
|
||||
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
|
||||
{
|
||||
ValueTask<SecretVoidFileMeta?> Get(Guid id);
|
||||
ValueTask<VoidFileMeta?> GetPublic(Guid id);
|
||||
ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta;
|
||||
|
||||
ValueTask Set(Guid id, SecretVoidFileMeta meta);
|
||||
|
||||
|
@ -4,8 +4,6 @@ namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface IFileStore
|
||||
{
|
||||
ValueTask<PublicVoidFile?> Get(Guid id);
|
||||
|
||||
ValueTask<PrivateVoidFile> Ingress(IngressPayload payload, CancellationToken cts);
|
||||
|
||||
ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts);
|
||||
|
@ -6,6 +6,6 @@ public interface IUserStore
|
||||
{
|
||||
ValueTask<Guid?> LookupUser(string email);
|
||||
ValueTask<T?> Get<T>(Guid id) where T : VoidUser;
|
||||
ValueTask Set(PrivateVoidUser user);
|
||||
ValueTask<PagedResult<PublicVoidUser>> ListUsers(PagedRequest request);
|
||||
ValueTask Set(InternalVoidUser user);
|
||||
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);
|
||||
}
|
||||
|
||||
public ValueTask<SecretVoidFileMeta?> Get(Guid id)
|
||||
{
|
||||
return GetMeta<SecretVoidFileMeta>(id);
|
||||
return GetMeta<TMeta>(id);
|
||||
}
|
||||
|
||||
public async ValueTask Set(Guid id, SecretVoidFileMeta meta)
|
||||
@ -42,7 +37,7 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
|
||||
|
||||
public async ValueTask Update(Guid id, SecretVoidFileMeta patch)
|
||||
{
|
||||
var oldMeta = await Get(id);
|
||||
var oldMeta = await Get<SecretVoidFileMeta>(id);
|
||||
if (oldMeta?.EditSecret != patch.EditSecret)
|
||||
{
|
||||
throw new VoidNotAllowedException("Edit secret incorrect");
|
||||
@ -74,4 +69,4 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
|
||||
|
||||
private string MapMeta(Guid id) =>
|
||||
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)
|
||||
{
|
||||
var path = MapPath(request.Id);
|
||||
@ -68,10 +52,9 @@ public class LocalDiskFileStore : IFileStore
|
||||
{
|
||||
var id = payload.Id ?? Guid.NewGuid();
|
||||
var fPath = MapPath(id);
|
||||
SecretVoidFileMeta? vf = null;
|
||||
var vf = payload.Meta;
|
||||
if (payload.IsAppend)
|
||||
{
|
||||
vf = await _metadataStore.Get(payload.Id!.Value);
|
||||
if (vf?.EditSecret != null && vf.EditSecret != payload.EditSecret)
|
||||
{
|
||||
throw new VoidNotAllowedException("Edit secret incorrect!");
|
||||
@ -98,13 +81,8 @@ public class LocalDiskFileStore : IFileStore
|
||||
}
|
||||
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,
|
||||
EditSecret = Guid.NewGuid(),
|
||||
Size = total
|
||||
@ -146,10 +124,14 @@ public class LocalDiskFileStore : IFileStore
|
||||
{
|
||||
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)
|
||||
{
|
||||
yield return loaded;
|
||||
yield return new()
|
||||
{
|
||||
Id = gid,
|
||||
Metadata = loaded
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ public class UserManager : IUserManager
|
||||
var userId = await _store.LookupUser(email);
|
||||
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");
|
||||
|
||||
user.LastLogin = DateTimeOffset.UtcNow;
|
||||
@ -32,7 +32,7 @@ public class UserManager : IUserManager
|
||||
var existingUser = await _store.LookupUser(email);
|
||||
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,
|
||||
LastLogin = DateTimeOffset.UtcNow
|
||||
|
@ -23,14 +23,14 @@ public class UserStore : IUserStore
|
||||
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.AddToList(UserList, 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);
|
||||
users = (request.SortBy, request.SortOrder) switch
|
||||
@ -40,9 +40,9 @@ public class UserStore : IUserStore
|
||||
_ => 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)
|
||||
{
|
||||
if (user != default)
|
||||
|
@ -36,7 +36,8 @@ export function useApi() {
|
||||
createOrder: (id) => getJson("GET", `/upload/${id}/paywall`),
|
||||
getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`),
|
||||
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 DefaultAvatar = "https://i.imgur.com/8A5Fu65.jpeg";
|
||||
|
||||
/**
|
||||
* @constant {number} - Size of 1 kiB
|
||||
*/
|
||||
|
@ -1,9 +1,9 @@
|
||||
.preview {
|
||||
text-align: center;
|
||||
margin-top: 2vh;
|
||||
width: 720px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview > a {
|
||||
|
@ -9,6 +9,7 @@ import {useApi} from "./Api";
|
||||
import {Helmet} from "react-helmet";
|
||||
import {FormatBytes} from "./Util";
|
||||
import {ApiHost} from "./Const";
|
||||
import {FileUploader} from "./FileUploader";
|
||||
|
||||
export function FilePreview() {
|
||||
const {Api} = useApi();
|
||||
@ -121,6 +122,7 @@ export function FilePreview() {
|
||||
{FormatBytes(info?.metadata?.size ?? 0, 2)}
|
||||
</div>
|
||||
</div>
|
||||
{info.uploader ? <FileUploader uploader={info.uploader}/> : null}
|
||||
<FileEdit file={info}/>
|
||||
</Fragment>
|
||||
) : "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() {
|
||||
return (
|
||||
<h1>Coming soon..</h1>
|
||||
);
|
||||
const [profile, setProfile] = useState();
|
||||
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