Add profiles

This commit is contained in:
Kieran 2022-02-27 13:54:25 +00:00
parent b1f5ca88f8
commit 0a946d8f74
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
26 changed files with 293 additions and 100 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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)
{ {
} }
} }

View File

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

View File

@ -0,0 +1,8 @@
using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
public interface IFileInfoManager
{
ValueTask<PublicVoidFile?> Get(Guid id);
}

View File

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

View File

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

View File

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

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

View File

@ -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");
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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
*/ */

View File

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

View File

@ -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"}

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

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

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

View File

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