Co-authored-by: Kieran <kieran@harkin.me>
Reviewed-on: Kieran/void.cat#65
This commit is contained in:
Kieran 2023-05-09 13:56:57 +00:00
parent 8289122347
commit 4de977c1dd
197 changed files with 8010 additions and 5170 deletions

19
.drone.yml Normal file
View File

@ -0,0 +1,19 @@
---
kind: pipeline
type: kubernetes
name: default
metadata:
namespace: git
steps:
- name: build
image: r.j3ss.co/img
privileged: true
environment:
TOKEN:
from_secret: token
commands:
- img login -u kieran -p $TOKEN git.v0l.io
- img build -t git.v0l.io/kieran/void-cat:latest .
- img push git.v0l.io/kieran/void-cat:latest

View File

@ -1,23 +1,26 @@
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app
#install npm
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash -
RUN apt-get install -y nodejs
#run yarn install
COPY VoidCat/spa/package.json VoidCat/spa/yarn.lock spa/
RUN cd spa && npx yarn install
# Copy everything else and build
COPY . .
RUN rm -rf VoidCat/appsettings.*.json
RUN dotnet publish -c Release -o out VoidCat/VoidCat.csproj
RUN cd VoidCat/spa \
&& npx yarn \
&& npx yarn build
RUN rm -rf VoidCat/appsettings.*.json \
&& git config --global --add safe.directory /app \
&& dotnet publish -c Release -o out VoidCat/VoidCat.csproj
# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
RUN apt update && apt install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
RUN apt update \
&& apt install -y --no-install-recommends ffmpeg \
&& apt clean \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "VoidCat.dll"]

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using VoidCat.Model;
using VoidCat.Model.User;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
@ -34,16 +34,16 @@ public class AdminController : Controller
/// <returns></returns>
[HttpPost]
[Route("file")]
public async Task<RenderedResults<PublicVoidFile>> ListFiles([FromBody] PagedRequest request)
public async Task<RenderedResults<VoidFileResponse>> ListFiles([FromBody] PagedRequest request)
{
var files = await _fileMetadata.ListFiles<FileMeta>(request);
var files = await _fileMetadata.ListFiles(request);
return new()
{
Page = files.Page,
PageSize = files.PageSize,
TotalResults = files.TotalResults,
Results = (await files.Results.SelectAwait(a => _fileInfo.Get(a.Id)).ToListAsync())!
Results = (await files.Results.SelectAwait(a => _fileInfo.Get(a.Id, false)).ToListAsync())!
};
}
@ -74,7 +74,7 @@ public class AdminController : Controller
var ret = await result.Results.SelectAwait(async a =>
{
var uploads = await _userUploads.ListFiles(a.Id, new(0, int.MaxValue));
return new AdminListedUser(a, uploads.TotalResults);
return new AdminListedUser(a.ToAdminApiUser(true), uploads.TotalResults);
}).ToListAsync();
return new()
@ -93,14 +93,27 @@ public class AdminController : Controller
/// <returns></returns>
[HttpPost]
[Route("update-user")]
public async Task<IActionResult> UpdateUser([FromBody] PrivateUser user)
public async Task<IActionResult> UpdateUser([FromBody] AdminUpdateUser user)
{
var oldUser = await _userStore.Get(user.Id);
if (oldUser == default) return BadRequest();
await _userStore.AdminUpdateUser(user);
oldUser.Storage = user.Storage;
oldUser.Email = user.Email;
await _userStore.AdminUpdateUser(oldUser);
return Ok();
}
public record AdminListedUser(PrivateUser User, int Uploads);
public record AdminListedUser(AdminApiUser User, int Uploads);
public class AdminUpdateUser
{
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
public string Email { get; init; } = null!;
public string Storage { get; init; } = null!;
}
}

View File

@ -4,8 +4,8 @@ using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using VoidCat.Database;
using VoidCat.Model;
using VoidCat.Model.User;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Users;
@ -57,7 +57,7 @@ public class AuthController : Controller
var user = await _manager.Login(req.Username, req.Password);
var token = CreateToken(user, DateTime.UtcNow.AddHours(12));
var tokenWriter = new JwtSecurityTokenHandler();
return new(tokenWriter.WriteToken(token), Profile: user.ToPublic());
return new(tokenWriter.WriteToken(token), Profile: user.ToApiUser(true));
}
catch (Exception ex)
{
@ -91,7 +91,7 @@ public class AuthController : Controller
var newUser = await _manager.Register(req.Username, req.Password);
var token = CreateToken(newUser, DateTime.UtcNow.AddHours(12));
var tokenWriter = new JwtSecurityTokenHandler();
return new(tokenWriter.WriteToken(token), Profile: newUser.ToPublic());
return new(tokenWriter.WriteToken(token), Profile: newUser.ToApiUser(true));
}
catch (Exception ex)
{
@ -189,7 +189,7 @@ public class AuthController : Controller
new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
};
claims.AddRange(user.Roles.Select(a => new Claim(ClaimTypes.Role, a)));
claims.AddRange(user.Roles.Select(a => new Claim(ClaimTypes.Role, a.Role)));
return new JwtSecurityToken(_settings.JwtSettings.Issuer, claims: claims,
signingCredentials: credentials);
@ -210,7 +210,7 @@ public class AuthController : Controller
public string? Captcha { get; init; }
}
public sealed record LoginResponse(string? Jwt, string? Error = null, User? Profile = null);
public sealed record LoginResponse(string? Jwt, string? Error = null, ApiUser? Profile = null);
public sealed record CreateApiKeyRequest(DateTime Expiry);
}

View File

@ -1,8 +1,8 @@
using System.Net;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using VoidCat.Database;
using VoidCat.Model;
using VoidCat.Model.Payments;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
@ -52,7 +52,7 @@ public class DownloadController : Controller
if (id.EndsWith(".torrent"))
{
var t = await voidFile.Metadata!.MakeTorrent(
var t = await voidFile.Metadata.MakeTorrent(voidFile.Id,
await _storage.Open(new(gid, Enumerable.Empty<RangeRequest>()), CancellationToken.None),
_settings.SiteUrl, _settings.TorrentTrackers);
@ -107,9 +107,9 @@ public class DownloadController : Controller
await Response.CompleteAsync();
}
private async Task<PublicVoidFile?> SetupDownload(Guid id)
private async Task<VoidFileResponse?> SetupDownload(Guid id)
{
var meta = await _fileInfo.Get(id);
var meta = await _fileInfo.Get(id, false);
if (meta == null)
{
Response.StatusCode = 404;
@ -117,10 +117,10 @@ public class DownloadController : Controller
}
// check payment order
if (meta.Payment != default && meta.Payment.Service != PaymentServices.None && meta.Payment.Required)
if (meta.Payment != default && meta.Payment.Service != PaywallService.None && meta.Payment.Required)
{
var orderId = Request.Headers.GetHeader("V-OrderId") ?? Request.Query["orderId"];
if (!await IsOrderPaid(orderId))
if (!await IsOrderPaid(orderId!))
{
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
return default;
@ -128,7 +128,7 @@ public class DownloadController : Controller
}
// prevent hot-linking viruses
var referer = Request.Headers.Referer.Count > 0 ? new Uri(Request.Headers.Referer.First()) : null;
var referer = Request.Headers.Referer.Count > 0 ? new Uri(Request.Headers.Referer.First()!) : null;
var hasCorrectReferer = referer?.Host.Equals(_settings.SiteUrl.Host, StringComparison.InvariantCultureIgnoreCase) ??
false;
@ -151,7 +151,7 @@ public class DownloadController : Controller
if (Guid.TryParse(orderId, out var oid))
{
var order = await _paymentOrders.Get(oid);
if (order?.Status == PaymentOrderStatus.Paid)
if (order?.Status == PaywallOrderStatus.Paid)
{
return true;
}

View File

@ -40,14 +40,16 @@ public class IndexController : Controller
var jsonManifest = await System.IO.File.ReadAllTextAsync(manifestPath);
return View("~/Pages/Index.cshtml", new IndexModel
{
Meta = await _fileMetadata.Get(gid),
Id = gid,
Meta = (await _fileMetadata.Get(gid))?.ToMeta(false),
Manifest = JsonConvert.DeserializeObject<AssetManifest>(jsonManifest)!
});
}
public class IndexModel
{
public FileMeta? Meta { get; init; }
public Guid Id { get; init; }
public VoidFileMeta? Meta { get; init; }
public AssetManifest Manifest { get; init; }
}

View File

@ -3,11 +3,11 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.StaticFiles;
using Newtonsoft.Json;
using VoidCat.Database;
using VoidCat.Model;
using VoidCat.Model.Payments;
using VoidCat.Model.User;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
using File = VoidCat.Database.File;
namespace VoidCat.Controllers
{
@ -63,6 +63,7 @@ namespace VoidCat.Controllers
{
throw new InvalidOperationException("Site is in maintenance mode");
}
var uid = HttpContext.GetUserId();
var mime = Request.Headers.GetHeader("V-Content-Type");
var filename = Request.Headers.GetHeader("V-Filename");
@ -86,14 +87,14 @@ namespace VoidCat.Controllers
var store = _settings.DefaultFileStore;
if (uid.HasValue)
{
var user = await _userStore.Get<InternalUser>(uid.Value);
var user = await _userStore.Get(uid.Value);
if (user?.Storage != default)
{
store = user.Storage!;
}
}
var meta = new SecretFileMeta
var meta = new File
{
MimeType = mime,
Name = filename,
@ -109,7 +110,7 @@ namespace VoidCat.Controllers
HttpContext.RequestAborted);
// save metadata
await _metadata.Set(vf.Id, vf.Metadata!);
await _metadata.Add(vf);
// attach file upload to user
if (uid.HasValue)
@ -126,7 +127,7 @@ namespace VoidCat.Controllers
return Content(urlBuilder.Uri.ToString(), "text/plain");
}
return Json(UploadResult.Success(vf));
return Json(UploadResult.Success(vf.ToResponse(true)));
}
catch (Exception ex)
{
@ -158,8 +159,9 @@ namespace VoidCat.Controllers
{
throw new InvalidOperationException("Site is in maintenance mode");
}
var gid = id.FromBase58Guid();
var meta = await _metadata.Get<SecretFileMeta>(gid);
var meta = await _metadata.Get(gid);
if (meta == default) return UploadResult.Error("File not found");
// Parse V-Segment header
@ -182,8 +184,8 @@ namespace VoidCat.Controllers
}, HttpContext.RequestAborted);
// update file size
await _metadata.Set(vf.Id, vf.Metadata!);
return UploadResult.Success(vf);
await _metadata.Update(vf.Id, vf);
return UploadResult.Success(vf.ToResponse(true));
}
catch (Exception ex)
{
@ -205,7 +207,10 @@ namespace VoidCat.Controllers
var uid = HttpContext.GetUserId();
var isOwner = uid.HasValue && await _userUploads.Uploader(fid) == uid;
return isOwner ? Json(await _fileInfo.GetPrivate(fid)) : Json(await _fileInfo.Get(fid));
var info = await _fileInfo.Get(fid, isOwner);
if (info == default) return StatusCode(404);
return Json(info);
}
/// <summary>
@ -232,10 +237,10 @@ namespace VoidCat.Controllers
/// <returns></returns>
[HttpGet]
[Route("{id}/payment")]
public async ValueTask<PaymentOrder?> CreateOrder([FromRoute] string id)
public async ValueTask<PaywallOrder?> CreateOrder([FromRoute] string id)
{
var gid = id.FromBase58Guid();
var file = await _fileInfo.Get(gid);
var file = await _fileInfo.Get(gid, false);
var config = await _paymentStore.Get(gid);
var provider = await _paymentFactory.CreateProvider(config!.Service);
@ -250,7 +255,7 @@ namespace VoidCat.Controllers
/// <returns></returns>
[HttpGet]
[Route("{id}/payment/{order:guid}")]
public async ValueTask<PaymentOrder?> 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 _paymentStore.Get(gid);
@ -270,17 +275,23 @@ namespace VoidCat.Controllers
public async Task<IActionResult> SetPaymentConfig([FromRoute] string id, [FromBody] SetPaymentConfigRequest req)
{
var gid = id.FromBase58Guid();
var meta = await _metadata.Get<SecretFileMeta>(gid);
var meta = await _metadata.Get(gid);
if (meta == default) return NotFound();
if (!meta.CanEdit(req.EditSecret)) return Unauthorized();
if (req.Strike != default)
if (req.StrikeHandle != default)
{
await _paymentStore.Add(gid, new StrikePaymentConfig()
await _paymentStore.Delete(gid);
await _paymentStore.Add(gid, new Paywall
{
Service = PaymentServices.Strike,
Handle = req.Strike.Handle,
Cost = req.Strike.Cost,
File = meta,
Service = PaywallService.Strike,
PaywallStrike = new()
{
Handle = req.StrikeHandle
},
Amount = req.Amount,
Currency = Enum.Parse<PaywallCurrency>(req.Currency),
Required = req.Required
});
@ -303,14 +314,21 @@ namespace VoidCat.Controllers
/// </remarks>
[HttpPost]
[Route("{id}/meta")]
public async Task<IActionResult> UpdateFileMeta([FromRoute] string id, [FromBody] SecretFileMeta fileMeta)
public async Task<IActionResult> UpdateFileMeta([FromRoute] string id, [FromBody] VoidFileMeta fileMeta)
{
var gid = id.FromBase58Guid();
var meta = await _metadata.Get<SecretFileMeta>(gid);
var meta = await _metadata.Get(gid);
if (meta == default) return NotFound();
if (!meta.CanEdit(fileMeta.EditSecret)) return Unauthorized();
await _metadata.Update(gid, fileMeta);
await _metadata.Update(gid, new()
{
Name = fileMeta.Name,
Description = fileMeta.Description,
Expires = fileMeta.Expires,
MimeType = fileMeta.MimeType
});
return Ok();
}
@ -349,9 +367,9 @@ namespace VoidCat.Controllers
}
}
public record UploadResult(bool Ok, PrivateVoidFile? File, string? ErrorMessage)
public record UploadResult(bool Ok, VoidFileResponse? File, string? ErrorMessage)
{
public static UploadResult Success(PrivateVoidFile vf)
public static UploadResult Success(VoidFileResponse vf)
=> new(true, vf, null);
public static UploadResult Error(string message)
@ -363,7 +381,11 @@ namespace VoidCat.Controllers
[JsonConverter(typeof(Base58GuidConverter))]
public Guid EditSecret { get; init; }
public StrikePaymentConfig? Strike { get; init; }
public decimal Amount { get; init; }
public string Currency { get; init; } = null!;
public string? StrikeHandle { get; init; }
public bool Required { get; init; }
}

View File

@ -1,8 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using VoidCat.Database;
using VoidCat.Model;
using VoidCat.Model.User;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
using UserFlags = VoidCat.Database.UserFlags;
namespace VoidCat.Controllers;
@ -41,18 +42,11 @@ public class UserController : Controller
if (isMe && !loggedUser.HasValue) return Unauthorized();
var requestedId = isMe ? loggedUser!.Value : id.FromBase58Guid();
if (loggedUser == requestedId)
{
var pUser = await _store.Get<PrivateUser>(requestedId);
if (pUser == default) return NotFound();
var user = await _store.Get(requestedId);
if (loggedUser != requestedId && !(user?.Flags.HasFlag(UserFlags.PublicProfile) ?? false))
return NotFound();
return Json(pUser);
}
var user = await _store.Get<PublicUser>(requestedId);
if (!(user?.Flags.HasFlag(UserFlags.PublicProfile) ?? false)) return NotFound();
return Json(user);
return Json(user!.ToApiUser(isMe));
}
/// <summary>
@ -63,14 +57,20 @@ public class UserController : Controller
/// <param name="user"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> UpdateUser([FromRoute] string id, [FromBody] PublicUser user)
public async Task<IActionResult> UpdateUser([FromRoute] string id, [FromBody] ApiUser user)
{
var loggedUser = await GetAuthorizedUser(id);
if (loggedUser == default) return Unauthorized();
if (!loggedUser.Flags.HasFlag(UserFlags.EmailVerified)) return Forbid();
await _store.UpdateProfile(user);
loggedUser.Avatar = user.Avatar;
loggedUser.DisplayName = user.Name ?? "void user";
loggedUser.Flags = UserFlags.EmailVerified | (user.PublicProfile ? UserFlags.PublicProfile : 0) |
(user.PublicUploads ? UserFlags.PublicUploads : 0);
await _store.UpdateProfile(loggedUser);
return Ok();
}
@ -102,13 +102,13 @@ public class UserController : Controller
var results = await _userUploads.ListFiles(id.FromBase58Guid(), request);
var files = await results.Results.ToListAsync();
var fileInfo = await Task.WhenAll(files.Select(a => _fileInfoManager.Get(a).AsTask()));
return Json(new RenderedResults<PublicVoidFile>()
var fileInfo = await _fileInfoManager.Get(files.ToArray(), false);
return Json(new RenderedResults<VoidFileResponse>()
{
PageSize = results.PageSize,
Page = results.Page,
TotalResults = results.TotalResults,
Results = fileInfo.Where(a => a != null).ToList()!
Results = fileInfo.ToList()
});
}
@ -148,21 +148,21 @@ public class UserController : Controller
if (!await _emailVerification.VerifyCode(user, token)) return BadRequest();
user.Flags |= UserFlags.EmailVerified;
await _store.UpdateProfile(user.ToPublic());
await _store.UpdateProfile(user);
return Accepted();
}
private async Task<InternalUser?> GetAuthorizedUser(string id)
private async Task<User?> GetAuthorizedUser(string id)
{
var loggedUser = HttpContext.GetUserId();
var gid = id.FromBase58Guid();
var user = await _store.Get<InternalUser>(gid);
var user = await _store.Get(gid);
return user?.Id != loggedUser ? default : user;
}
private async Task<InternalUser?> GetRequestedUser(string id)
private async Task<User?> GetRequestedUser(string id)
{
var gid = id.FromBase58Guid();
return await _store.Get<InternalUser>(gid);
return await _store.Get(gid);
}
}

View File

@ -0,0 +1,11 @@
namespace VoidCat.Database;
public class ApiKey
{
public Guid Id { get; init; } = Guid.NewGuid();
public Guid UserId { get; init; }
public User User { get; init; } = null!;
public string Token { get; init; } = null!;
public DateTime Expiry { get; init; }
public DateTime Created { get; init; }
}

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class ApiKeyConfiguration : IEntityTypeConfiguration<ApiKey>
{
public void Configure(EntityTypeBuilder<ApiKey> builder)
{
builder.ToTable("ApiKey");
builder.HasKey(a => a.Id);
builder.Property(a => a.Token)
.IsRequired();
builder.Property(a => a.Expiry)
.IsRequired();
builder.Property(a => a.Created)
.IsRequired();
builder.HasOne(a => a.User)
.WithMany()
.HasForeignKey(a => a.UserId);
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class EmailVerficationConfiguration : IEntityTypeConfiguration<EmailVerification>
{
public void Configure(EntityTypeBuilder<EmailVerification> builder)
{
builder.ToTable("EmailVerification");
builder.HasKey(a => a.Id);
builder.Property(a => a.Code)
.IsRequired();
builder.Property(a => a.Expires)
.IsRequired();
builder.HasOne(a => a.User)
.WithMany()
.HasForeignKey(a => a.UserId);
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class FileConfiguration : IEntityTypeConfiguration<File>
{
public void Configure(EntityTypeBuilder<File> builder)
{
builder.ToTable("Files");
builder.HasKey(a => a.Id);
builder.Property(a => a.Name);
builder.Property(a => a.Size)
.IsRequired();
builder.Property(a => a.Uploaded)
.IsRequired();
builder.Property(a => a.Description);
builder.Property(a => a.MimeType)
.IsRequired()
.HasDefaultValue("application/octet-stream");
builder.Property(a => a.Digest);
builder.Property(a => a.EditSecret)
.IsRequired();
builder.Property(a => a.Expires);
builder.Property(a => a.Storage)
.IsRequired()
.HasDefaultValue("local-disk");
builder.Property(a => a.EncryptionParams);
builder.Property(a => a.MagnetLink);
builder.HasIndex(a => a.Uploaded);
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class PaywallConfiguration : IEntityTypeConfiguration<Paywall>
{
public void Configure(EntityTypeBuilder<Paywall> builder)
{
builder.ToTable("Payment");
builder.HasKey(a => a.Id);
builder.Property(a => a.Service)
.IsRequired();
builder.Property(a => a.Currency)
.IsRequired();
builder.Property(a => a.Amount)
.IsRequired();
builder.Property(a => a.Required)
.IsRequired();
builder.HasOne(a => a.File)
.WithOne(a => a.Paywall)
.HasForeignKey<Paywall>();
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class PaywallOrderConfiguration : IEntityTypeConfiguration<PaywallOrder>
{
public void Configure(EntityTypeBuilder<PaywallOrder> builder)
{
builder.ToTable("PaymentOrder");
builder.HasKey(a => a.Id);
builder.Property(a => a.Service)
.IsRequired();
builder.Property(a => a.Currency)
.IsRequired();
builder.Property(a => a.Amount)
.IsRequired();
builder.Property(a => a.Status)
.IsRequired();
builder.HasIndex(a => a.Status);
builder.HasOne(a => a.File)
.WithMany()
.HasForeignKey(a => a.FileId);
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class PaywallOrderLightningConfiguration : IEntityTypeConfiguration<PaywallOrderLightning>
{
public void Configure(EntityTypeBuilder<PaywallOrderLightning> builder)
{
builder.ToTable("PaymentOrderLightning");
builder.HasKey(a => a.OrderId);
builder.Property(a => a.Invoice)
.IsRequired();
builder.Property(a => a.Expire)
.IsRequired();
builder.HasOne(a => a.Order)
.WithOne(a => a.OrderLightning)
.HasForeignKey<PaywallOrderLightning>(a => a.OrderId);
}
}

View File

@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class PaywallStrikeConfiguration : IEntityTypeConfiguration<PaywallStrike>
{
public void Configure(EntityTypeBuilder<PaywallStrike> builder)
{
builder.ToTable("PaymentStrike");
builder.HasKey(a => a.Id);
builder.Property(a => a.Handle)
.IsRequired();
builder.HasOne(a => a.Paywall)
.WithOne(a => a.PaywallStrike)
.HasForeignKey<PaywallStrike>();
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class UserAuthTokenConfiguration : IEntityTypeConfiguration<UserAuthToken>
{
public void Configure(EntityTypeBuilder<UserAuthToken> builder)
{
builder.ToTable("UsersAuthToken");
builder.HasKey(a => a.Id);
builder.Property(a => a.Provider)
.IsRequired();
builder.Property(a => a.AccessToken)
.IsRequired();
builder.Property(a => a.TokenType)
.IsRequired();
builder.Property(a => a.Expires)
.IsRequired();
builder.Property(a => a.RefreshToken)
.IsRequired();
builder.Property(a => a.Scope)
.IsRequired();
builder.Property(a => a.IdToken);
builder.HasOne(a => a.User)
.WithMany()
.HasForeignKey(a => a.UserId);
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("Users");
builder.HasKey(a => a.Id);
builder.Property(a => a.Email)
.IsRequired();
builder.Property(a => a.Password)
.IsRequired(false);
builder.Property(a => a.Created)
.IsRequired();
builder.Property(a => a.LastLogin);
builder.Property(a => a.DisplayName)
.IsRequired()
.HasDefaultValue("void user");
builder.Property(a => a.Flags)
.IsRequired();
builder.Property(a => a.Storage)
.IsRequired()
.HasDefaultValue("local-disk");
builder.Property(a => a.AuthType)
.IsRequired();
builder.HasIndex(a => a.Email);
}
}

View File

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class UserFileConfiguration : IEntityTypeConfiguration<UserFile>
{
public void Configure(EntityTypeBuilder<UserFile> builder)
{
builder.ToTable("UserFiles");
builder.HasKey(a => new {a.UserId, a.FileId});
builder.HasOne(a => a.User)
.WithMany(a => a.UserFiles)
.HasForeignKey(a => a.UserId);
builder.HasOne(a => a.File)
.WithOne()
.HasForeignKey<UserFile>(a => a.FileId);
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class UserRolesConfiguration : IEntityTypeConfiguration<UserRole>
{
public void Configure(EntityTypeBuilder<UserRole> builder)
{
builder.ToTable("UserRoles");
builder.HasKey(a => new {a.UserId, a.Role});
builder.Property(a => a.Role)
.IsRequired();
builder.HasOne(a => a.User)
.WithMany(a => a.Roles)
.HasForeignKey(a => a.UserId);
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace VoidCat.Database.Configurations;
public class VirusScanResultConfiguration : IEntityTypeConfiguration<VirusScanResult>
{
public void Configure(EntityTypeBuilder<VirusScanResult> builder)
{
builder.ToTable("VirusScanResult");
builder.HasKey(a => a.Id);
builder.Property(a => a.Scanner)
.IsRequired();
builder.Property(a => a.Score)
.IsRequired();
builder.Property(a => a.Names);
builder.HasOne(a => a.File)
.WithMany()
.HasForeignKey(a => a.FileId);
}
}

View File

@ -0,0 +1,11 @@
namespace VoidCat.Database;
public class EmailVerification
{
public Guid Id { get; init; } = Guid.NewGuid();
public Guid UserId { get; init; }
public User User { get; init; } = null!;
public Guid Code { get; init; } = Guid.NewGuid();
public DateTime Expires { get; init; }
}

19
VoidCat/Database/File.cs Normal file
View File

@ -0,0 +1,19 @@
namespace VoidCat.Database;
public record File
{
public Guid Id { get; init; }
public string? Name { get; set; }
public ulong Size { get; init; }
public DateTime Uploaded { get; init; } = DateTime.UtcNow;
public string? Description { get; set; }
public string MimeType { get; set; } = "application/octet-stream";
public string? Digest { get; init; }
public Guid EditSecret { get; init; }
public DateTime? Expires { get; set; }
public string Storage { get; set; } = "local-disk";
public string? EncryptionParams { get; set; }
public string? MagnetLink { get; set; }
public Paywall? Paywall { get; init; }
}

View File

@ -0,0 +1,47 @@
namespace VoidCat.Database;
public enum PaywallCurrency : byte
{
BTC = 0,
USD = 1,
EUR = 2,
GBP = 3
}
public enum PaywallService
{
/// <summary>
/// No service
/// </summary>
None,
/// <summary>
/// Strike.me payment service
/// </summary>
Strike,
/// <summary>
/// LNProxy payment
/// </summary>
LnProxy,
}
public class Paywall
{
public Guid Id { get; init; } = Guid.NewGuid();
public File File { get; init; } = null!;
public PaywallService Service { get; init; }
public PaywallCurrency Currency { get; init; }
public decimal Amount { get; init; }
public bool Required { get; init; } = true;
public PaywallStrike? PaywallStrike { get; init; }
}
public class PaywallStrike
{
public Guid Id { get; init; } = Guid.NewGuid();
public Paywall Paywall { get; init; } = null!;
public string Handle { get; init; } = null!;
}

View File

@ -0,0 +1,40 @@
namespace VoidCat.Database;
public class PaywallOrder
{
public Guid Id { get; init; }
public Guid FileId { get; init; }
public File File { get; init; } = null!;
public PaywallService Service { get; init; }
public PaywallCurrency Currency { get; init; }
public decimal Amount { get; init; }
public PaywallOrderStatus Status { get; set; }
public PaywallOrderLightning? OrderLightning { get; init; }
}
public enum PaywallOrderStatus : byte
{
/// <summary>
/// Invoice is not paid yet
/// </summary>
Unpaid = 0,
/// <summary>
/// Invoice is paid
/// </summary>
Paid = 1,
/// <summary>
/// Invoice has expired and cant be paid
/// </summary>
Expired = 2
}
public class PaywallOrderLightning
{
public Guid OrderId { get; init; }
public PaywallOrder Order { get; init; } = null!;
public string Invoice { get; init; } = null!;
public DateTime Expire { get; init; }
}

114
VoidCat/Database/User.cs Normal file
View File

@ -0,0 +1,114 @@
namespace VoidCat.Database;
[Flags]
public enum UserFlags
{
/// <summary>
/// Profile is public
/// </summary>
PublicProfile = 1,
/// <summary>
/// Uploads list is public
/// </summary>
PublicUploads = 2,
/// <summary>
/// Account has email verified
/// </summary>
EmailVerified = 4
}
public sealed class User
{
/// <summary>
/// Unique Id of the user
/// </summary>
public Guid Id { get; init; }
/// <summary>
/// Users email address
/// </summary>
public string Email { get; set; } = null!;
/// <summary>
/// Users password (hashed)
/// </summary>
public string? Password { get; init; }
/// <summary>
/// When the user account was created
/// </summary>
public DateTime Created { get; init; } = DateTime.UtcNow;
/// <summary>
/// The last time the user logged in
/// </summary>
public DateTime? 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>
/// Profile flags
/// </summary>
public UserFlags Flags { get; set; } = UserFlags.PublicProfile;
/// <summary>
/// Users storage system for new uploads
/// </summary>
public string Storage { get; set; } = "local-disk";
/// <summary>
/// Account authentication type
/// </summary>
public UserAuthType AuthType { get; init; }
/// <summary>
/// Roles assigned to this user which grant them extra permissions
/// </summary>
public List<UserRole> Roles { get; init; } = new();
/// <summary>
/// All files uploaded by this user
/// </summary>
public List<UserFile> UserFiles { get; init; } = new();
}
public class UserRole
{
public Guid UserId { get; init; }
public User User { get; init; }
public string Role { get; init; } = null!;
}
public enum UserAuthType
{
/// <summary>
/// Encrypted password
/// </summary>
Internal = 0,
/// <summary>
/// PGP challenge
/// </summary>
PGP = 1,
/// <summary>
/// OAuth2 token
/// </summary>
OAuth2 = 2,
/// <summary>
/// Lightning node challenge
/// </summary>
Lightning = 3
}

View File

@ -0,0 +1,15 @@
namespace VoidCat.Database;
public class UserAuthToken
{
public Guid Id { get; init; }
public Guid UserId { get; init; }
public User User { get; init; } = null!;
public string Provider { get; init; } = null!;
public string AccessToken { get; init; } = null!;
public string TokenType { get; init; } = null!;
public DateTime Expires { get; init; }
public string RefreshToken { get; init; } = null!;
public string Scope { get; init; } = null!;
public string? IdToken { get; init; }
}

View File

@ -0,0 +1,10 @@
namespace VoidCat.Database;
public class UserFile
{
public Guid FileId { get; init; }
public File File { get; init; }
public Guid UserId { get; init; }
public User User { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace VoidCat.Database;
public class VirusScanResult
{
public Guid Id { get; init; } = Guid.NewGuid();
public Guid FileId { get; init; }
public File File { get; init; } = null!;
public DateTime ScanTime { get; init; }
public string Scanner { get; init; } = null!;
public decimal Score { get; init; }
public string Names { get; init; } = null!;
}

View File

@ -0,0 +1,353 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using VoidCat.Services;
#nullable disable
namespace VoidCat.Migrations
{
[DbContext(typeof(VoidContext))]
[Migration("20230503115108_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("VoidCat.Database.ApiKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("Expiry")
.HasColumnType("timestamp with time zone");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ApiKey", (string)null);
});
modelBuilder.Entity("VoidCat.Database.EmailVerification", b =>
{
b.Property<Guid>("Code")
.HasColumnType("uuid");
b.Property<DateTime>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasIndex("UserId");
b.ToTable("EmailVerification", (string)null);
});
modelBuilder.Entity("VoidCat.Database.File", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Digest")
.HasColumnType("text");
b.Property<Guid>("EditSecret")
.HasColumnType("uuid");
b.Property<string>("EncryptionParams")
.HasColumnType("text");
b.Property<DateTime?>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<string>("MagnetLink")
.HasColumnType("text");
b.Property<string>("MimeType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("application/octet-stream");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<decimal>("Size")
.HasColumnType("numeric(20,0)");
b.Property<string>("Storage")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("local-disk");
b.Property<DateTime>("Uploaded")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Uploaded");
b.ToTable("Files", (string)null);
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AuthType")
.HasColumnType("integer");
b.Property<string>("Avatar")
.HasColumnType("text");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("void user");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Flags")
.HasColumnType("integer");
b.Property<DateTime?>("LastLogin")
.HasColumnType("timestamp with time zone");
b.Property<string>("Password")
.HasColumnType("text");
b.Property<string>("Storage")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("local-disk");
b.HasKey("Id");
b.HasIndex("Email");
b.ToTable("Users", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserAuthToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<string>("IdToken")
.HasColumnType("text");
b.Property<string>("Provider")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RefreshToken")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Scope")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TokenType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UsersAuthToken", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserFile", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.HasKey("UserId", "FileId");
b.HasIndex("FileId")
.IsUnique();
b.ToTable("UserFiles", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserRole", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("Role")
.HasColumnType("text");
b.HasKey("UserId", "Role");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("VoidCat.Database.VirusScanResult", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.Property<string>("Names")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ScanTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Scanner")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("Score")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("FileId");
b.ToTable("VirusScanResult", (string)null);
});
modelBuilder.Entity("VoidCat.Database.ApiKey", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.EmailVerification", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.UserAuthToken", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.UserFile", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithOne()
.HasForeignKey("VoidCat.Database.UserFile", "FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("VoidCat.Database.User", "User")
.WithMany("UserFiles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.UserRole", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany("Roles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.VirusScanResult", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithMany()
.HasForeignKey("FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Navigation("Roles");
b.Navigation("UserFiles");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,223 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace VoidCat.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Files",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "text", nullable: true),
Size = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
Uploaded = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Description = table.Column<string>(type: "text", nullable: true),
MimeType = table.Column<string>(type: "text", nullable: false, defaultValue: "application/octet-stream"),
Digest = table.Column<string>(type: "text", nullable: true),
EditSecret = table.Column<Guid>(type: "uuid", nullable: false),
Expires = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Storage = table.Column<string>(type: "text", nullable: false, defaultValue: "local-disk"),
EncryptionParams = table.Column<string>(type: "text", nullable: true),
MagnetLink = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Files", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: true),
Created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
LastLogin = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Avatar = table.Column<string>(type: "text", nullable: true),
DisplayName = table.Column<string>(type: "text", nullable: false, defaultValue: "void user"),
Flags = table.Column<int>(type: "integer", nullable: false),
Storage = table.Column<string>(type: "text", nullable: false, defaultValue: "local-disk"),
AuthType = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "VirusScanResult",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
FileId = table.Column<Guid>(type: "uuid", nullable: false),
ScanTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Scanner = table.Column<string>(type: "text", nullable: false),
Score = table.Column<decimal>(type: "numeric", nullable: false),
Names = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_VirusScanResult", x => x.Id);
table.ForeignKey(
name: "FK_VirusScanResult_Files_FileId",
column: x => x.FileId,
principalTable: "Files",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiKey",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
Token = table.Column<string>(type: "text", nullable: false),
Expiry = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiKey", x => x.Id);
table.ForeignKey(
name: "FK_ApiKey_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserFiles",
columns: table => new
{
FileId = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserFiles", x => new { x.UserId, x.FileId });
table.ForeignKey(
name: "FK_UserFiles_Files_FileId",
column: x => x.FileId,
principalTable: "Files",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserFiles_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserRoles",
columns: table => new
{
UserId = table.Column<Guid>(type: "uuid", nullable: false),
Role = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.Role });
table.ForeignKey(
name: "FK_UserRoles_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UsersAuthToken",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
Provider = table.Column<string>(type: "text", nullable: false),
AccessToken = table.Column<string>(type: "text", nullable: false),
TokenType = table.Column<string>(type: "text", nullable: false),
Expires = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
RefreshToken = table.Column<string>(type: "text", nullable: false),
Scope = table.Column<string>(type: "text", nullable: false),
IdToken = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UsersAuthToken", x => x.Id);
table.ForeignKey(
name: "FK_UsersAuthToken_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ApiKey_UserId",
table: "ApiKey",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Files_Uploaded",
table: "Files",
column: "Uploaded");
migrationBuilder.CreateIndex(
name: "IX_UserFiles_FileId",
table: "UserFiles",
column: "FileId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_Email",
table: "Users",
column: "Email");
migrationBuilder.CreateIndex(
name: "IX_UsersAuthToken_UserId",
table: "UsersAuthToken",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_VirusScanResult_FileId",
table: "VirusScanResult",
column: "FileId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApiKey");
migrationBuilder.DropTable(
name: "UserFiles");
migrationBuilder.DropTable(
name: "UserRoles");
migrationBuilder.DropTable(
name: "UsersAuthToken");
migrationBuilder.DropTable(
name: "VirusScanResult");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "Files");
}
}
}

View File

@ -0,0 +1,497 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using VoidCat.Services;
#nullable disable
namespace VoidCat.Migrations
{
[DbContext(typeof(VoidContext))]
[Migration("20230503120701_Paywall")]
partial class Paywall
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("VoidCat.Database.ApiKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("Expiry")
.HasColumnType("timestamp with time zone");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ApiKey", (string)null);
});
modelBuilder.Entity("VoidCat.Database.EmailVerification", b =>
{
b.Property<Guid>("Code")
.HasColumnType("uuid");
b.Property<DateTime>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasIndex("UserId");
b.ToTable("EmailVerification", (string)null);
});
modelBuilder.Entity("VoidCat.Database.File", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Digest")
.HasColumnType("text");
b.Property<Guid>("EditSecret")
.HasColumnType("uuid");
b.Property<string>("EncryptionParams")
.HasColumnType("text");
b.Property<DateTime?>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<string>("MagnetLink")
.HasColumnType("text");
b.Property<string>("MimeType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("application/octet-stream");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<decimal>("Size")
.HasColumnType("numeric(20,0)");
b.Property<string>("Storage")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("local-disk");
b.Property<DateTime>("Uploaded")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Uploaded");
b.ToTable("Files", (string)null);
});
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<byte>("Currency")
.HasColumnType("smallint");
b.Property<bool>("Required")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<int>("Service")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Payment", (string)null);
});
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<byte>("Currency")
.HasColumnType("smallint");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.Property<int>("Service")
.HasColumnType("integer");
b.Property<byte>("Status")
.HasColumnType("smallint");
b.HasKey("Id");
b.HasIndex("FileId");
b.HasIndex("Status");
b.ToTable("PaymentOrder", (string)null);
});
modelBuilder.Entity("VoidCat.Database.PaywallOrderLightning", b =>
{
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime>("Expire")
.HasColumnType("timestamp with time zone");
b.Property<string>("Invoice")
.IsRequired()
.HasColumnType("text");
b.HasKey("OrderId");
b.ToTable("PaymentOrderLightning", (string)null);
});
modelBuilder.Entity("VoidCat.Database.PaywallStrike", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<string>("Handle")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("PaymentStrike", (string)null);
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AuthType")
.HasColumnType("integer");
b.Property<string>("Avatar")
.HasColumnType("text");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("void user");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Flags")
.HasColumnType("integer");
b.Property<DateTime?>("LastLogin")
.HasColumnType("timestamp with time zone");
b.Property<string>("Password")
.HasColumnType("text");
b.Property<string>("Storage")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("local-disk");
b.HasKey("Id");
b.HasIndex("Email");
b.ToTable("Users", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserAuthToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<string>("IdToken")
.HasColumnType("text");
b.Property<string>("Provider")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RefreshToken")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Scope")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TokenType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UsersAuthToken", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserFile", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.HasKey("UserId", "FileId");
b.HasIndex("FileId")
.IsUnique();
b.ToTable("UserFiles", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserRole", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("Role")
.HasColumnType("text");
b.HasKey("UserId", "Role");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("VoidCat.Database.VirusScanResult", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.Property<string>("Names")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ScanTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Scanner")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("Score")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("FileId");
b.ToTable("VirusScanResult", (string)null);
});
modelBuilder.Entity("VoidCat.Database.ApiKey", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.EmailVerification", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithOne("Paywall")
.HasForeignKey("VoidCat.Database.Paywall", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
});
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithMany()
.HasForeignKey("FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
});
modelBuilder.Entity("VoidCat.Database.PaywallOrderLightning", b =>
{
b.HasOne("VoidCat.Database.PaywallOrder", "Order")
.WithOne("OrderLightning")
.HasForeignKey("VoidCat.Database.PaywallOrderLightning", "OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("VoidCat.Database.PaywallStrike", b =>
{
b.HasOne("VoidCat.Database.Paywall", "Paywall")
.WithOne("PaywallStrike")
.HasForeignKey("VoidCat.Database.PaywallStrike", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Paywall");
});
modelBuilder.Entity("VoidCat.Database.UserAuthToken", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.UserFile", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithOne()
.HasForeignKey("VoidCat.Database.UserFile", "FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("VoidCat.Database.User", "User")
.WithMany("UserFiles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.UserRole", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany("Roles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.VirusScanResult", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithMany()
.HasForeignKey("FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
});
modelBuilder.Entity("VoidCat.Database.File", b =>
{
b.Navigation("Paywall");
});
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
{
b.Navigation("PaywallStrike");
});
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
{
b.Navigation("OrderLightning");
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Navigation("Roles");
b.Navigation("UserFiles");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,121 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace VoidCat.Migrations
{
/// <inheritdoc />
public partial class Paywall : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Payment",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Service = table.Column<int>(type: "integer", nullable: false),
Currency = table.Column<byte>(type: "smallint", nullable: false),
Amount = table.Column<decimal>(type: "numeric", nullable: false),
Required = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Payment", x => x.Id);
table.ForeignKey(
name: "FK_Payment_Files_Id",
column: x => x.Id,
principalTable: "Files",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PaymentOrder",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
FileId = table.Column<Guid>(type: "uuid", nullable: false),
Service = table.Column<int>(type: "integer", nullable: false),
Currency = table.Column<byte>(type: "smallint", nullable: false),
Amount = table.Column<decimal>(type: "numeric", nullable: false),
Status = table.Column<byte>(type: "smallint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PaymentOrder", x => x.Id);
table.ForeignKey(
name: "FK_PaymentOrder_Files_FileId",
column: x => x.FileId,
principalTable: "Files",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PaymentStrike",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Handle = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PaymentStrike", x => x.Id);
table.ForeignKey(
name: "FK_PaymentStrike_Payment_Id",
column: x => x.Id,
principalTable: "Payment",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PaymentOrderLightning",
columns: table => new
{
OrderId = table.Column<Guid>(type: "uuid", nullable: false),
Invoice = table.Column<string>(type: "text", nullable: false),
Expire = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PaymentOrderLightning", x => x.OrderId);
table.ForeignKey(
name: "FK_PaymentOrderLightning_PaymentOrder_OrderId",
column: x => x.OrderId,
principalTable: "PaymentOrder",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PaymentOrder_FileId",
table: "PaymentOrder",
column: "FileId");
migrationBuilder.CreateIndex(
name: "IX_PaymentOrder_Status",
table: "PaymentOrder",
column: "Status");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PaymentOrderLightning");
migrationBuilder.DropTable(
name: "PaymentStrike");
migrationBuilder.DropTable(
name: "PaymentOrder");
migrationBuilder.DropTable(
name: "Payment");
}
}
}

View File

@ -0,0 +1,501 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using VoidCat.Services;
#nullable disable
namespace VoidCat.Migrations
{
[DbContext(typeof(VoidContext))]
[Migration("20230508205513_EmailVerificationId")]
partial class EmailVerificationId
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("VoidCat.Database.ApiKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("Expiry")
.HasColumnType("timestamp with time zone");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ApiKey", (string)null);
});
modelBuilder.Entity("VoidCat.Database.EmailVerification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("Code")
.HasColumnType("uuid");
b.Property<DateTime>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("EmailVerification", (string)null);
});
modelBuilder.Entity("VoidCat.Database.File", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Digest")
.HasColumnType("text");
b.Property<Guid>("EditSecret")
.HasColumnType("uuid");
b.Property<string>("EncryptionParams")
.HasColumnType("text");
b.Property<DateTime?>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<string>("MagnetLink")
.HasColumnType("text");
b.Property<string>("MimeType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("application/octet-stream");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<decimal>("Size")
.HasColumnType("numeric(20,0)");
b.Property<string>("Storage")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("local-disk");
b.Property<DateTime>("Uploaded")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Uploaded");
b.ToTable("Files", (string)null);
});
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<byte>("Currency")
.HasColumnType("smallint");
b.Property<bool>("Required")
.HasColumnType("boolean");
b.Property<int>("Service")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Payment", (string)null);
});
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<byte>("Currency")
.HasColumnType("smallint");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.Property<int>("Service")
.HasColumnType("integer");
b.Property<byte>("Status")
.HasColumnType("smallint");
b.HasKey("Id");
b.HasIndex("FileId");
b.HasIndex("Status");
b.ToTable("PaymentOrder", (string)null);
});
modelBuilder.Entity("VoidCat.Database.PaywallOrderLightning", b =>
{
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime>("Expire")
.HasColumnType("timestamp with time zone");
b.Property<string>("Invoice")
.IsRequired()
.HasColumnType("text");
b.HasKey("OrderId");
b.ToTable("PaymentOrderLightning", (string)null);
});
modelBuilder.Entity("VoidCat.Database.PaywallStrike", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<string>("Handle")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("PaymentStrike", (string)null);
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AuthType")
.HasColumnType("integer");
b.Property<string>("Avatar")
.HasColumnType("text");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("void user");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Flags")
.HasColumnType("integer");
b.Property<DateTime?>("LastLogin")
.HasColumnType("timestamp with time zone");
b.Property<string>("Password")
.HasColumnType("text");
b.Property<string>("Storage")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("local-disk");
b.HasKey("Id");
b.HasIndex("Email");
b.ToTable("Users", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserAuthToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<string>("IdToken")
.HasColumnType("text");
b.Property<string>("Provider")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RefreshToken")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Scope")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TokenType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UsersAuthToken", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserFile", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.HasKey("UserId", "FileId");
b.HasIndex("FileId")
.IsUnique();
b.ToTable("UserFiles", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserRole", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("Role")
.HasColumnType("text");
b.HasKey("UserId", "Role");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("VoidCat.Database.VirusScanResult", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.Property<string>("Names")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ScanTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Scanner")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("Score")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("FileId");
b.ToTable("VirusScanResult", (string)null);
});
modelBuilder.Entity("VoidCat.Database.ApiKey", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.EmailVerification", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithOne("Paywall")
.HasForeignKey("VoidCat.Database.Paywall", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
});
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithMany()
.HasForeignKey("FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
});
modelBuilder.Entity("VoidCat.Database.PaywallOrderLightning", b =>
{
b.HasOne("VoidCat.Database.PaywallOrder", "Order")
.WithOne("OrderLightning")
.HasForeignKey("VoidCat.Database.PaywallOrderLightning", "OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("VoidCat.Database.PaywallStrike", b =>
{
b.HasOne("VoidCat.Database.Paywall", "Paywall")
.WithOne("PaywallStrike")
.HasForeignKey("VoidCat.Database.PaywallStrike", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Paywall");
});
modelBuilder.Entity("VoidCat.Database.UserAuthToken", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.UserFile", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithOne()
.HasForeignKey("VoidCat.Database.UserFile", "FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("VoidCat.Database.User", "User")
.WithMany("UserFiles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.UserRole", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany("Roles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.VirusScanResult", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithMany()
.HasForeignKey("FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
});
modelBuilder.Entity("VoidCat.Database.File", b =>
{
b.Navigation("Paywall");
});
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
{
b.Navigation("PaywallStrike");
});
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
{
b.Navigation("OrderLightning");
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Navigation("Roles");
b.Navigation("UserFiles");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,63 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace VoidCat.Migrations
{
/// <inheritdoc />
public partial class EmailVerificationId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EmailVerification",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
Code = table.Column<Guid>(type: "uuid", nullable: false),
Expires = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.ForeignKey(
name: "FK_EmailVerification_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.AlterColumn<bool>(
name: "Required",
table: "Payment",
type: "boolean",
nullable: false,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: true);
migrationBuilder.AddPrimaryKey(
name: "PK_EmailVerification",
table: "EmailVerification",
column: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable("EmailVerification");
migrationBuilder.AlterColumn<bool>(
name: "Required",
table: "Payment",
type: "boolean",
nullable: false,
defaultValue: true,
oldClrType: typeof(bool),
oldType: "boolean");
}
}
}

View File

@ -0,0 +1,498 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using VoidCat.Services;
#nullable disable
namespace VoidCat.Migrations
{
[DbContext(typeof(VoidContext))]
partial class VoidContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("VoidCat.Database.ApiKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("Expiry")
.HasColumnType("timestamp with time zone");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ApiKey", (string)null);
});
modelBuilder.Entity("VoidCat.Database.EmailVerification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("Code")
.HasColumnType("uuid");
b.Property<DateTime>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("EmailVerification", (string)null);
});
modelBuilder.Entity("VoidCat.Database.File", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Digest")
.HasColumnType("text");
b.Property<Guid>("EditSecret")
.HasColumnType("uuid");
b.Property<string>("EncryptionParams")
.HasColumnType("text");
b.Property<DateTime?>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<string>("MagnetLink")
.HasColumnType("text");
b.Property<string>("MimeType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("application/octet-stream");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<decimal>("Size")
.HasColumnType("numeric(20,0)");
b.Property<string>("Storage")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("local-disk");
b.Property<DateTime>("Uploaded")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Uploaded");
b.ToTable("Files", (string)null);
});
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<byte>("Currency")
.HasColumnType("smallint");
b.Property<bool>("Required")
.HasColumnType("boolean");
b.Property<int>("Service")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Payment", (string)null);
});
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<byte>("Currency")
.HasColumnType("smallint");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.Property<int>("Service")
.HasColumnType("integer");
b.Property<byte>("Status")
.HasColumnType("smallint");
b.HasKey("Id");
b.HasIndex("FileId");
b.HasIndex("Status");
b.ToTable("PaymentOrder", (string)null);
});
modelBuilder.Entity("VoidCat.Database.PaywallOrderLightning", b =>
{
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime>("Expire")
.HasColumnType("timestamp with time zone");
b.Property<string>("Invoice")
.IsRequired()
.HasColumnType("text");
b.HasKey("OrderId");
b.ToTable("PaymentOrderLightning", (string)null);
});
modelBuilder.Entity("VoidCat.Database.PaywallStrike", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<string>("Handle")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("PaymentStrike", (string)null);
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AuthType")
.HasColumnType("integer");
b.Property<string>("Avatar")
.HasColumnType("text");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("void user");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Flags")
.HasColumnType("integer");
b.Property<DateTime?>("LastLogin")
.HasColumnType("timestamp with time zone");
b.Property<string>("Password")
.HasColumnType("text");
b.Property<string>("Storage")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("local-disk");
b.HasKey("Id");
b.HasIndex("Email");
b.ToTable("Users", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserAuthToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<string>("IdToken")
.HasColumnType("text");
b.Property<string>("Provider")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RefreshToken")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Scope")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TokenType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UsersAuthToken", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserFile", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.HasKey("UserId", "FileId");
b.HasIndex("FileId")
.IsUnique();
b.ToTable("UserFiles", (string)null);
});
modelBuilder.Entity("VoidCat.Database.UserRole", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("Role")
.HasColumnType("text");
b.HasKey("UserId", "Role");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("VoidCat.Database.VirusScanResult", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.Property<string>("Names")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ScanTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Scanner")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("Score")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("FileId");
b.ToTable("VirusScanResult", (string)null);
});
modelBuilder.Entity("VoidCat.Database.ApiKey", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.EmailVerification", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithOne("Paywall")
.HasForeignKey("VoidCat.Database.Paywall", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
});
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithMany()
.HasForeignKey("FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
});
modelBuilder.Entity("VoidCat.Database.PaywallOrderLightning", b =>
{
b.HasOne("VoidCat.Database.PaywallOrder", "Order")
.WithOne("OrderLightning")
.HasForeignKey("VoidCat.Database.PaywallOrderLightning", "OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("VoidCat.Database.PaywallStrike", b =>
{
b.HasOne("VoidCat.Database.Paywall", "Paywall")
.WithOne("PaywallStrike")
.HasForeignKey("VoidCat.Database.PaywallStrike", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Paywall");
});
modelBuilder.Entity("VoidCat.Database.UserAuthToken", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.UserFile", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithOne()
.HasForeignKey("VoidCat.Database.UserFile", "FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("VoidCat.Database.User", "User")
.WithMany("UserFiles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.UserRole", b =>
{
b.HasOne("VoidCat.Database.User", "User")
.WithMany("Roles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("VoidCat.Database.VirusScanResult", b =>
{
b.HasOne("VoidCat.Database.File", "File")
.WithMany()
.HasForeignKey("FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("File");
});
modelBuilder.Entity("VoidCat.Database.File", b =>
{
b.Navigation("Paywall");
});
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
{
b.Navigation("PaywallStrike");
});
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
{
b.Navigation("OrderLightning");
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Navigation("Roles");
b.Navigation("UserFiles");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,7 @@
namespace VoidCat.Model;
public class AdminApiUser : ApiUser
{
public string Storage { get; init; } = null!;
public string Email { get; init; } = null!;
}

51
VoidCat/Model/ApiUser.cs Normal file
View File

@ -0,0 +1,51 @@
using Newtonsoft.Json;
using VoidCat.Database;
namespace VoidCat.Model;
/// <summary>
/// A user object which can be returned via the API
/// </summary>
public class ApiUser
{
/// <summary>
/// Unique Id of the user
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
/// <summary>
/// Display name
/// </summary>
public string? Name { get; init; }
/// <summary>
/// Avatar
/// </summary>
public string? Avatar { get; init; }
/// <summary>
/// If profile can be viewed by anyone
/// </summary>
public bool PublicProfile { get; init; }
/// <summary>
/// If the users uploads can be viewed by anyone
/// </summary>
public bool PublicUploads { get; init; }
/// <summary>
/// If the account is not email verified
/// </summary>
public bool? NeedsVerification { get; init; }
/// <summary>
/// A list of roles the user has
/// </summary>
public List<string> Roles { get; init; } = new();
/// <summary>
/// When the account was created
/// </summary>
public DateTime Created { get; init; }
}

View File

@ -1,9 +0,0 @@
namespace VoidCat.Model;
/// <summary>
/// Email verification token
/// </summary>
/// <param name="Id"></param>
/// <param name="User"></param>
/// <param name="Expires"></param>
public sealed record EmailVerificationCode(Guid User, Guid Code, DateTime Expires);

View File

@ -6,8 +6,9 @@ using Amazon.Runtime;
using Amazon.S3;
using BencodeNET.Objects;
using BencodeNET.Torrents;
using VoidCat.Database;
using VoidCat.Model.Exceptions;
using VoidCat.Model.User;
using File = VoidCat.Database.File;
namespace VoidCat.Model;
@ -82,7 +83,7 @@ public static class Extensions
return !string.IsNullOrEmpty(h.Value.ToString()) ? h.Value.ToString() : default;
}
public static bool CanEdit(this SecretFileMeta file, Guid? editSecret)
public static bool CanEdit(this File file, Guid? editSecret)
{
return file.EditSecret == editSecret;
}
@ -231,12 +232,15 @@ public static class Extensions
/// <param name="password"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public static bool CheckPassword(this InternalUser vu, string password)
public static bool CheckPassword(this Database.User vu, string password)
{
if (vu.AuthType != AuthType.Internal)
if (vu.AuthType != UserAuthType.Internal)
throw new InvalidOperationException("User type is not internal, cannot check password!");
var hashParts = vu.Password.Split(":");
if (string.IsNullOrEmpty(vu.Password))
throw new InvalidOperationException("User password is not set");
var hashParts = vu.Password!.Split(":");
return vu.Password == password.Hash(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
}
@ -245,7 +249,7 @@ public static class Extensions
/// </summary>
/// <param name="oldMeta"></param>
/// <param name="meta"></param>
public static void Patch(this FileMeta oldMeta, FileMeta meta)
public static void Patch(this File oldMeta, File meta)
{
oldMeta.Description = meta.Description ?? oldMeta.Description;
oldMeta.Name = meta.Name ?? oldMeta.Name;
@ -276,13 +280,14 @@ public static class Extensions
public static bool HasGoogle(this VoidSettings settings)
=> settings.Google != null;
public static async Task<Torrent> MakeTorrent(this FileMeta meta, Stream fileStream, Uri baseAddress, List<string> trackers)
public static async Task<Torrent> MakeTorrent(this VoidFileMeta meta, Guid id, Stream fileStream, Uri baseAddress,
List<string> trackers)
{
const int pieceSize = 262_144;
const int pieceHashLen = 20;
var webSeed = new UriBuilder(baseAddress)
{
Path = $"/d/{meta.Id.ToBase58()}"
Path = $"/d/{id.ToBase58()}"
};
async Task<byte[]> BuildPieces()
@ -310,7 +315,7 @@ public static class Extensions
FileSize = (long)meta.Size
},
Comment = meta.Name,
CreationDate = meta.Uploaded.UtcDateTime,
CreationDate = meta.Uploaded,
IsPrivate = false,
PieceSize = pieceSize,
Pieces = await BuildPieces(),
@ -324,4 +329,72 @@ public static class Extensions
return t;
}
}
public static VoidFileResponse ToResponse(this File f, bool withEditSecret)
{
return new()
{
Id = f.Id,
Metadata = f.ToMeta(withEditSecret)
};
}
public static VoidFileMeta ToMeta(this File f, bool withEditSecret)
{
return new()
{
Name = f.Name,
Description = f.Description,
Uploaded = f.Uploaded,
MimeType = f.MimeType,
Size = f.Size,
Digest = f.Digest,
Expires = f.Expires,
EditSecret = withEditSecret ? f.EditSecret : null,
Storage = f.Storage,
EncryptionParams = f.EncryptionParams,
MagnetLink = f.MagnetLink
};
}
public static ApiUser ToApiUser(this User u, bool isSelf)
{
return new()
{
Id = u.Id,
Name = u.DisplayName,
Avatar = u.Avatar,
Created = u.Created,
NeedsVerification = isSelf ? !u.Flags.HasFlag(UserFlags.EmailVerified) : null,
PublicProfile = u.Flags.HasFlag(UserFlags.PublicProfile),
PublicUploads = u.Flags.HasFlag(UserFlags.PublicUploads),
Roles = u.Roles.Select(a => a.Role).ToList()
};
}
public static AdminApiUser ToAdminApiUser(this User u, bool isSelf)
{
return new()
{
Id = u.Id,
Name = u.DisplayName,
Avatar = u.Avatar,
Created = u.Created,
NeedsVerification = isSelf ? !u.Flags.HasFlag(UserFlags.EmailVerified) : null,
PublicProfile = u.Flags.HasFlag(UserFlags.PublicProfile),
PublicUploads = u.Flags.HasFlag(UserFlags.PublicUploads),
Roles = u.Roles.Select(a => a.Role).ToList(),
Storage = u.Storage,
Email = u.Email
};
}
public static VirusStatus ToVirusStatus(this VirusScanResult r)
{
return new()
{
ScanTime = r.ScanTime,
IsVirus = r.Score > 0.7m,
Names = r.Names
};
}
}

View File

@ -1,6 +1,6 @@
namespace VoidCat.Model;
public sealed record IngressPayload(Stream InStream, SecretFileMeta Meta, int Segment, int TotalSegments, bool ShouldStripMetadata)
public sealed record IngressPayload(Stream InStream, Database.File Meta, int Segment, int TotalSegments, bool ShouldStripMetadata)
{
public Guid Id { get; init; } = Guid.NewGuid();
public Guid? EditSecret { get; init; }

View File

@ -1,22 +0,0 @@
using System.Text.Json.Serialization;
namespace VoidCat.Model.Payments;
/// <summary>
/// Money amount for payment orders
/// </summary>
/// <param name="Amount"></param>
/// <param name="Currency"></param>
public record PaymentMoney(decimal Amount, PaymentCurrencies Currency);
/// <summary>
/// Supported payment currencies
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PaymentCurrencies : byte
{
BTC = 0,
USD = 1,
EUR = 2,
GBP = 3
}

View File

@ -1,69 +0,0 @@
namespace VoidCat.Model.Payments;
/// <summary>
/// Status of payment order
/// </summary>
public enum PaymentOrderStatus : byte
{
/// <summary>
/// Invoice is not paid yet
/// </summary>
Unpaid,
/// <summary>
/// Invoice is paid
/// </summary>
Paid,
/// <summary>
/// Invoice has expired and cant be paid
/// </summary>
Expired
}
/// <summary>
/// Base payment order
/// </summary>
public class PaymentOrder
{
/// <summary>
/// Unique id of the order
/// </summary>
public Guid Id { get; init; }
/// <summary>
/// File id this order is for
/// </summary>
public Guid File { get; init; }
/// <summary>
/// Service used to generate this order
/// </summary>
public PaymentServices Service { get; init; }
/// <summary>
/// The price of the order
/// </summary>
public PaymentMoney Price { get; init; } = null!;
/// <summary>
/// Current status of the order
/// </summary>
public PaymentOrderStatus Status { get; set; }
}
/// <summary>
/// A payment order lightning network invoice
/// </summary>
public class LightningPaymentOrder : PaymentOrder
{
/// <summary>
/// Lightning invoice
/// </summary>
public string Invoice { get; init; } = null!;
/// <summary>
/// Expire time of the order
/// </summary>
public DateTime Expire { get; init; }
}

View File

@ -1,17 +0,0 @@
namespace VoidCat.Model.Payments;
/// <summary>
/// Payment services supported by the system
/// </summary>
public enum PaymentServices
{
/// <summary>
/// No service
/// </summary>
None,
/// <summary>
/// Strike.me payment service
/// </summary>
Strike
}

View File

@ -1,45 +0,0 @@
namespace VoidCat.Model.Payments;
/// <summary>
/// Base payment config
/// </summary>
public abstract class PaymentConfig
{
/// <summary>
/// File this config is for
/// </summary>
public Guid File { get; init; }
/// <summary>
/// Service used to pay the payment
/// </summary>
public PaymentServices Service { get; init; } = PaymentServices.None;
/// <summary>
/// The cost for the payment to pass
/// </summary>
public PaymentMoney Cost { get; init; } = new(0m, PaymentCurrencies.BTC);
/// <summary>
/// If the payment is required
/// </summary>
public bool Required { get; init; } = true;
}
/// <inheritdoc />
public sealed class NoPaymentConfig : PaymentConfig
{
}
/// <summary>
/// Payment config for <see cref="PaymentServices.Strike"/> service
/// </summary>
/// <param name="Cost"></param>
public sealed class StrikePaymentConfig : PaymentConfig
{
/// <summary>
/// Strike username to pay to
/// </summary>
public string Handle { get; init; } = null!;
}

View File

@ -1,18 +0,0 @@
using Newtonsoft.Json;
namespace VoidCat.Model.User;
public sealed class ApiKey
{
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
[JsonConverter(typeof(Base58GuidConverter))]
public Guid UserId { get; init; }
public string Token { get; init; }
public DateTime Expiry { get; init; }
public DateTime Created { get; init; }
}

View File

@ -1,27 +0,0 @@
namespace VoidCat.Model.User;
/// <summary>
/// User account authentication type
/// </summary>
public enum AuthType
{
/// <summary>
/// Encrypted password
/// </summary>
Internal = 0,
/// <summary>
/// PGP challenge
/// </summary>
PGP = 1,
/// <summary>
/// OAuth2 token
/// </summary>
OAuth2 = 2,
/// <summary>
/// Lightning node challenge
/// </summary>
Lightning = 3
}

View File

@ -1,12 +0,0 @@
namespace VoidCat.Model.User;
/// <summary>
/// Internal user object used by the system
/// </summary>
public sealed class InternalUser : PrivateUser
{
/// <summary>
/// A password hash for the user in the format <see cref="Extensions.HashPassword"/>
/// </summary>
public string Password { get; init; } = null!;
}

View File

@ -1,17 +0,0 @@
namespace VoidCat.Model.User;
/// <summary>
/// A user object which includes the Email
/// </summary>
public class PrivateUser : User
{
/// <summary>
/// Users email address
/// </summary>
public string Email { get; set; } = null!;
/// <summary>
/// Users storage system for new uploads
/// </summary>
public string? Storage { get; set; }
}

View File

@ -1,6 +0,0 @@
namespace VoidCat.Model.User;
/// <inheritdoc />
public sealed class PublicUser : User
{
}

View File

@ -1,67 +0,0 @@
using Newtonsoft.Json;
namespace VoidCat.Model.User;
/// <summary>
/// The base user object for the system
/// </summary>
public abstract class User
{
/// <summary>
/// Unique Id of the user
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
/// <summary>
/// Roles assigned to this user which grant them extra permissions
/// </summary>
public HashSet<string> Roles { get; init; } = new() {Model.Roles.User};
/// <summary>
/// When the user account was created
/// </summary>
public DateTimeOffset Created { get; init; }
/// <summary>
/// The last time the user logged in
/// </summary>
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>
/// Profile flags
/// </summary>
public UserFlags Flags { get; set; } = UserFlags.PublicProfile;
/// <summary>
/// Account authentication type
/// </summary>
public AuthType AuthType { get; init; }
/// <summary>
/// Returns the Public object for this user
/// </summary>
/// <returns></returns>
public PublicUser ToPublic()
{
return new()
{
Id = Id,
Roles = Roles,
Created = Created,
LastLogin = LastLogin,
Avatar = Avatar,
Flags = Flags
};
}
}

View File

@ -1,25 +0,0 @@
namespace VoidCat.Model.User;
/// <summary>
/// OAuth2 access token
/// </summary>
public sealed class UserAuthToken
{
public Guid Id { get; init; }
public Guid User { get; init; }
public string Provider { get; init; }
public string AccessToken { get; init; }
public string TokenType { get; init; }
public DateTime Expires { get; init; }
public string RefreshToken { get; init; }
public string Scope { get; init; }
public string IdToken { get; init; }
}

View File

@ -1,23 +0,0 @@
namespace VoidCat.Model.User;
/// <summary>
/// Account status flags
/// </summary>
[Flags]
public enum UserFlags
{
/// <summary>
/// Profile is public
/// </summary>
PublicProfile = 1,
/// <summary>
/// Uploads list is public
/// </summary>
PublicUploads = 2,
/// <summary>
/// Account has email verified
/// </summary>
EmailVerified = 4
}

View File

@ -1,38 +1,14 @@
using Newtonsoft.Json;
namespace VoidCat.Model;
namespace VoidCat.Model;
/// <summary>
/// Results for virus scan of a single file
/// </summary>
public sealed class VirusScanResult
public sealed class VirusStatus
{
/// <summary>
/// Unique Id for this scan
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
/// <summary>
/// Id of the file that was scanned
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid File { get; init; }
/// <summary>
/// Time the file was scanned
/// </summary>
public DateTimeOffset ScanTime { get; init; } = DateTimeOffset.UtcNow;
/// <summary>
/// The name of the virus scanner software
/// </summary>
public string Scanner { get; init; } = null!;
/// <summary>
/// Virus detection score, this can mean different things for each scanner but the value should be between 0 and 1
/// </summary>
public decimal Score { get; init; }
public DateTime ScanTime { get; init; }
/// <summary>
/// Detected virus names
@ -42,5 +18,5 @@ public sealed class VirusScanResult
/// <summary>
/// If we consider this result as a virus or not
/// </summary>
public bool IsVirus => Score >= 0.75m && !string.IsNullOrEmpty(Names);
public bool IsVirus { get; init; }
}

View File

@ -1,48 +1,35 @@
using Newtonsoft.Json;
using VoidCat.Model.Payments;
using VoidCat.Model.User;
using VoidCat.Database;
namespace VoidCat.Model
namespace VoidCat.Model;
/// <summary>
/// Primary response type for file information
/// </summary>
public class VoidFileResponse
{
public abstract record VoidFile<TMeta> where TMeta : FileMeta
{
/// <summary>
/// Id of the file
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
public VoidFileMeta Metadata { get; init; } = null!;
public Paywall? Payment { get; init; }
public ApiUser? Uploader { get; init; }
public Bandwidth? Bandwidth { get; init; }
public VirusStatus? VirusScan { get; init; }
}
/// <summary>
/// Metadta related to the file
/// </summary>
public TMeta? Metadata { get; init; }
/// <summary>
/// Optional payment config
/// </summary>
public PaymentConfig? Payment { get; init; }
/// <summary>
/// User profile that uploaded the file
/// </summary>
public PublicUser? Uploader { get; init; }
/// <summary>
/// Traffic stats for this file
/// </summary>
public Bandwidth? Bandwidth { get; init; }
/// <summary>
/// Virus scanner results
/// </summary>
public VirusScanResult? VirusScan { get; init; }
}
public sealed record PublicVoidFile : VoidFile<FileMeta>
{
}
public sealed record PrivateVoidFile : VoidFile<SecretFileMeta>
{
}
public class VoidFileMeta
{
public string? Name { get; init; }
public ulong Size { get; init; }
public DateTime Uploaded { get; init; }
public string? Description { get; init; }
public string MimeType { get; init; }
public string? Digest { get; init; }
[JsonConverter(typeof(Base58GuidConverter))]
public Guid? EditSecret { get; init; }
public DateTime? Expires { get; init; }
public string Storage { get; init; } = "local-disk";
public string? EncryptionParams { get; init; }
public string? MagnetLink { get; init; }
}

View File

@ -1,100 +0,0 @@
using Newtonsoft.Json;
using VoidCat.Services.Abstractions;
// ReSharper disable InconsistentNaming
namespace VoidCat.Model;
/// <summary>
/// Base metadata must contain version number
/// </summary>
public interface IFileMeta
{
const int CurrentVersion = 3;
int Version { get; init; }
}
/// <summary>
/// File metadata which is managed by <see cref="IFileMetadataStore"/>
/// </summary>
public record FileMeta : IFileMeta
{
/// <summary>
/// Metadata version
/// </summary>
public int Version { get; init; } = IFileMeta.CurrentVersion;
/// <summary>
/// Internal Id of the file
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; set; }
/// <summary>
/// Filename
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Size of the file in storage
/// </summary>
public ulong Size { get; init; }
/// <summary>
/// Date file was uploaded
/// </summary>
public DateTimeOffset Uploaded { get; init; } = DateTimeOffset.UtcNow;
/// <summary>
/// Description about the file
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The content type of the file
/// </summary>
public string? MimeType { get; set; }
/// <summary>
/// SHA-256 hash of the file
/// </summary>
public string? Digest { get; set; }
/// <summary>
/// Url to download the file
/// </summary>
public Uri? Url { get; set; }
/// <summary>
/// Time when the file will expire and be deleted
/// </summary>
public DateTimeOffset? Expires { get; set; }
/// <summary>
/// What storage system the file is on
/// </summary>
public string? Storage { get; set; }
/// <summary>
/// Encryption params as JSON string
/// </summary>
public string? EncryptionParams { get; set; }
/// <summary>
/// Magnet link for downloads
/// </summary>
public string? MagnetLink { get; set; }
}
/// <summary>
/// <see cref="VoidFile"/> with attached <see cref="EditSecret"/>
/// </summary>
public record SecretFileMeta : FileMeta
{
/// <summary>
/// A secret key used to make edits to the file after its uploaded
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid EditSecret { get; init; }
}

View File

@ -127,6 +127,11 @@ namespace VoidCat.Model
"udp://tracker.openbittorrent.com:6969/announce",
"http://tracker.openbittorrent.com:80/announce"
};
/// <summary>
/// Lightning node configuration for LNProxy services
/// </summary>
public LndConfig? LndConfig { get; init; }
}
public sealed class TorSettings
@ -207,4 +212,12 @@ namespace VoidCat.Model
public string? ClientId { get; init; }
public string? ClientSecret { get; init; }
}
public sealed class LndConfig
{
public string Network { get; init; } = "regtest";
public Uri Endpoint { get; init; }
public string CertPath { get; init; } = null!;
public string MacaroonPath { get; init; } = null!;
}
}

View File

@ -1,6 +1,6 @@
@using VoidCat.Model
@using VoidCat.Services.Users
@model VoidCat.Model.EmailVerificationCode
@model VoidCat.Database.EmailVerification
<!DOCTYPE html>
<html lang="en">
@ -31,7 +31,7 @@
<div class="page">
<h1>void.cat</h1>
<p>Your verification code is below please copy this to complete verification</p>
<pre>@(Model?.Code.ToBase58() ?? "?????????????")</pre>
<pre>@(Model.Code.ToBase58())</pre>
<p>This code will expire in @BaseEmailVerification.HoursExpire hours</p>
</div>
</body>

View File

@ -17,12 +17,12 @@
<meta property="og:site_name" content="void.cat"/>
<meta property="og:title" content="@Model.Meta.Name"/>
<meta property="og:description" content="@Model.Meta.Description"/>
<meta property="og:url" content="@($"https://{Context.Request.Host}/{Model.Meta.Id.ToBase58()}")"/>
<meta property="og:url" content="@($"https://{Context.Request.Host}/{Model.Id.ToBase58()}")"/>
var mime = Model.Meta.MimeType;
if (mime != default)
{
var link = $"https://{Context.Request.Host}/d/{Model.Meta.Id.ToBase58()}";
var link = $"https://{Context.Request.Host}/d/{Model.Id.ToBase58()}";
if (mime.StartsWith("image/"))
{
<meta property="og:image" content="@link"/>

View File

@ -1,150 +1,188 @@
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Prometheus;
using VoidCat;
using VoidCat.Model;
using VoidCat.Services;
using VoidCat.Services.Analytics;
using VoidCat.Services.Migrations;
JsonConvert.DefaultSettings = () => VoidStartup.ConfigJsonSettings(new());
namespace VoidCat;
RunModes mode = args.Length == 0 ? RunModes.All : 0;
if (args.Contains("--run-webserver"))
static class Program
{
mode |= RunModes.Webserver;
}
if (args.Contains("--run-migrations"))
{
mode |= RunModes.Migrations;
}
if (args.Contains("--run-background-jobs"))
{
mode |= RunModes.BackgroundJobs;
}
Console.WriteLine($"Running with modes: {mode}");
async Task RunMigrations(IServiceProvider services)
{
using var migrationScope = services.CreateScope();
var migrations = migrationScope.ServiceProvider.GetServices<IMigration>();
var logger = migrationScope.ServiceProvider.GetRequiredService<ILogger<IMigration>>();
foreach (var migration in migrations.OrderBy(a => a.Order))
[Flags]
enum RunModes
{
logger.LogInformation("Running migration: {Migration}", migration.GetType().Name);
var res = await migration.Migrate(args);
logger.LogInformation("== Result: {Result}", res.ToString());
if (res == IMigration.MigrationResult.ExitCompleted)
Webserver = 1,
BackgroundJobs = 2,
Migrations = 4,
All = 255
}
public static async Task Main(string[] args)
{
JsonConvert.DefaultSettings = () => VoidStartup.ConfigJsonSettings(new());
RunModes mode = args.Length == 0 ? RunModes.All : 0;
if (args.Contains("--run-webserver"))
{
return;
mode |= RunModes.Webserver;
}
}
}
if (mode.HasFlag(RunModes.Webserver))
{
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
if (args.Contains("--run-migrations"))
{
mode |= RunModes.Migrations;
}
var configuration = builder.Configuration;
var voidSettings = configuration.GetSection("Settings").Get<VoidSettings>();
services.AddSingleton(voidSettings);
services.AddSingleton(voidSettings.Strike ?? new());
if (args.Contains("--run-background-jobs"))
{
mode |= RunModes.BackgroundJobs;
}
var seqSettings = configuration.GetSection("Seq");
builder.Logging.AddSeq(seqSettings);
Console.WriteLine($"Running with modes: {mode}");
services.AddBaseServices(voidSettings);
services.AddDatabaseServices(voidSettings);
services.AddWebServices(voidSettings);
async Task RunMigrations(IServiceProvider services)
{
using var migrationScope = services.CreateScope();
var migrations = migrationScope.ServiceProvider.GetServices<IMigration>();
var logger = migrationScope.ServiceProvider.GetRequiredService<ILogger<IMigration>>();
foreach (var migration in migrations.OrderBy(a => a.Order))
{
logger.LogInformation("Running migration: {Migration}", migration.GetType().Name);
var res = await migration.Migrate(args);
logger.LogInformation("== Result: {Result}", res.ToString());
if (res == IMigration.MigrationResult.ExitCompleted)
{
return;
}
}
}
if (mode.HasFlag(RunModes.Migrations))
{
services.AddMigrations(voidSettings);
}
if (mode.HasFlag(RunModes.Webserver))
{
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
if (mode.HasFlag(RunModes.BackgroundJobs))
{
services.AddBackgroundServices(voidSettings);
}
var configuration = builder.Configuration;
var voidSettings = configuration.GetSection("Settings").Get<VoidSettings>();
services.AddSingleton(voidSettings);
services.AddSingleton(voidSettings.Strike ?? new());
var app = builder.Build();
var seqSettings = configuration.GetSection("Seq");
builder.Logging.AddSeq(seqSettings);
if (mode.HasFlag(RunModes.Migrations))
{
await RunMigrations(app.Services);
}
ConfigureDb(services, voidSettings);
services.AddBaseServices(voidSettings);
services.AddDatabaseServices(voidSettings);
services.AddWebServices(voidSettings);
if (mode.HasFlag(RunModes.Migrations))
{
services.AddMigrations(voidSettings);
}
if (mode.HasFlag(RunModes.BackgroundJobs))
{
services.AddBackgroundServices(voidSettings);
}
var app = builder.Build();
if (mode.HasFlag(RunModes.Migrations))
{
await RunMigrations(app.Services);
}
#if HostSPA
app.UseStaticFiles();
app.UseStaticFiles();
#endif
app.UseHttpLogging();
app.UseRouting();
app.UseCors();
app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();
app.UseHttpLogging();
app.UseRouting();
app.UseCors();
app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();
app.UseHealthChecks("/healthz");
app.UseHealthChecks("/healthz");
app.UseMiddleware<AnalyticsMiddleware>();
app.UseEndpoints(ep =>
{
ep.MapControllers();
ep.MapMetrics();
ep.MapRazorPages();
app.UseMiddleware<AnalyticsMiddleware>();
app.UseEndpoints(ep =>
{
ep.MapControllers();
ep.MapMetrics();
ep.MapRazorPages();
#if HostSPA
ep.MapFallbackToFile("index.html");
ep.MapFallbackToFile("index.html");
#endif
});
});
app.Run();
}
else
{
// daemon style, dont run web server
var builder = Host.CreateDefaultBuilder(args);
builder.ConfigureServices((context, services) =>
{
var voidSettings = context.Configuration.GetSection("Settings").Get<VoidSettings>();
services.AddSingleton(voidSettings);
services.AddSingleton(voidSettings.Strike ?? new());
services.AddBaseServices(voidSettings);
services.AddDatabaseServices(voidSettings);
if (mode.HasFlag(RunModes.Migrations))
{
services.AddMigrations(voidSettings);
await app.RunAsync();
}
if (mode.HasFlag(RunModes.BackgroundJobs))
else
{
services.AddBackgroundServices(voidSettings);
}
});
builder.ConfigureLogging((context, logging) => { logging.AddSeq(context.Configuration.GetSection("Seq")); });
// daemon style, dont run web server
var builder = Host.CreateDefaultBuilder(args);
builder.ConfigureServices((context, services) =>
{
var voidSettings = context.Configuration.GetSection("Settings").Get<VoidSettings>();
services.AddSingleton(voidSettings);
services.AddSingleton(voidSettings.Strike ?? new());
var app = builder.Build();
if (mode.HasFlag(RunModes.Migrations))
{
await RunMigrations(app.Services);
ConfigureDb(services, voidSettings);
services.AddBaseServices(voidSettings);
services.AddDatabaseServices(voidSettings);
if (mode.HasFlag(RunModes.Migrations))
{
services.AddMigrations(voidSettings);
}
if (mode.HasFlag(RunModes.BackgroundJobs))
{
services.AddBackgroundServices(voidSettings);
}
});
builder.ConfigureLogging((context, logging) => { logging.AddSeq(context.Configuration.GetSection("Seq")); });
var app = builder.Build();
if (mode.HasFlag(RunModes.Migrations))
{
await RunMigrations(app.Services);
}
if (mode.HasFlag(RunModes.BackgroundJobs))
{
await app.RunAsync();
}
}
}
if (mode.HasFlag(RunModes.BackgroundJobs))
private static void ConfigureDb(IServiceCollection services, VoidSettings settings)
{
app.Run();
if (settings.HasPostgres())
{
services.AddDbContext<VoidContext>(o =>
o.UseNpgsql(settings.Postgres!));
}
}
/// <summary>
/// Dummy method for EF core migrations
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
// ReSharper disable once UnusedMember.Global
public static IHostBuilder CreateHostBuilder(string[] args)
{
var dummyHost = Host.CreateDefaultBuilder(args);
dummyHost.ConfigureServices((ctx, svc) =>
{
var settings = ctx.Configuration.GetSection("Settings").Get<VoidSettings>();
ConfigureDb(svc, settings);
});
return dummyHost;
}
}
[Flags]
internal enum RunModes
{
Webserver = 1,
BackgroundJobs = 2,
Migrations = 4,
All = 255
}

View File

@ -6,8 +6,8 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:7195",
"launchUrl": "https://localhost:3000",
"applicationUrl": "http://localhost:7195",
"launchUrl": "http://localhost:3000",
"dotnetRunMessages": true
},
"Docker": {

View File

@ -1,4 +1,4 @@
using VoidCat.Model.User;
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;

View File

@ -1,11 +1,21 @@
using VoidCat.Model;
using VoidCat.Model.User;
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
public interface IEmailVerification
{
ValueTask<EmailVerificationCode> SendNewCode(PrivateUser user);
/// <summary>
/// Send email verification code
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
ValueTask<EmailVerification> SendNewCode(User user);
ValueTask<bool> VerifyCode(PrivateUser user, Guid code);
/// <summary>
/// Perform account verification
/// </summary>
/// <param name="user"></param>
/// <param name="code"></param>
/// <returns></returns>
ValueTask<bool> VerifyCode(User user, Guid code);
}

View File

@ -1,44 +1,48 @@
using VoidCat.Model;
using File = VoidCat.Database.File;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// File metadata contains all data about a file except for the file data itself
/// </summary>
public interface IFileMetadataStore : IPublicPrivateStore<FileMeta, SecretFileMeta>
public interface IFileMetadataStore
{
/// <summary>
/// Get metadata for a single file
/// </summary>
/// <param name="id"></param>
/// <typeparam name="TMeta"></typeparam>
/// <returns></returns>
ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : FileMeta;
ValueTask<File?> Get(Guid id);
/// <summary>
/// Get metadata for multiple files
/// </summary>
/// <param name="ids"></param>
/// <typeparam name="TMeta"></typeparam>
/// <returns></returns>
ValueTask<IReadOnlyList<TMeta>> Get<TMeta>(Guid[] ids) where TMeta : FileMeta;
ValueTask<IReadOnlyList<File>> Get(Guid[] ids);
/// <summary>
/// Add new file metadata to this store
/// </summary>
/// <param name="f"></param>
/// <returns></returns>
ValueTask Add(File f);
/// <summary>
/// Update file metadata
/// </summary>
/// <param name="id"></param>
/// <param name="meta"></param>
/// <typeparam name="TMeta"></typeparam>
/// <returns></returns>
ValueTask Update<TMeta>(Guid id, TMeta meta) where TMeta : FileMeta;
ValueTask Update(Guid id, File meta);
/// <summary>
/// List all files in the store
/// </summary>
/// <param name="request"></param>
/// <typeparam name="TMeta"></typeparam>
/// <returns></returns>
ValueTask<PagedResult<TMeta>> ListFiles<TMeta>(PagedRequest request) where TMeta : FileMeta;
ValueTask<PagedResult<File>> ListFiles(PagedRequest request);
/// <summary>
/// Returns basic stats about the file store
@ -46,6 +50,13 @@ public interface IFileMetadataStore : IPublicPrivateStore<FileMeta, SecretFileMe
/// <returns></returns>
ValueTask<StoreStats> Stats();
/// <summary>
/// Delete metadata object from the store
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask Delete(Guid id);
/// <summary>
/// Simple stats of the current store
/// </summary>

View File

@ -1,4 +1,5 @@
using VoidCat.Model;
using File = VoidCat.Database.File;
namespace VoidCat.Services.Abstractions;
@ -18,7 +19,7 @@ public interface IFileStore
/// <param name="payload"></param>
/// <param name="cts"></param>
/// <returns></returns>
ValueTask<PrivateVoidFile> Ingress(IngressPayload payload, CancellationToken cts);
ValueTask<File> Ingress(IngressPayload payload, CancellationToken cts);
/// <summary>
/// Egress a file from the system (Download)

View File

@ -1,4 +1,4 @@
using VoidCat.Model.User;
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
@ -30,5 +30,5 @@ public interface IOAuthProvider
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
ValueTask<InternalUser?> GetUserDetails(UserAuthToken token);
ValueTask<User?> GetUserDetails(UserAuthToken token);
}

View File

@ -1,4 +1,4 @@
using VoidCat.Model.Payments;
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
@ -12,5 +12,5 @@ public interface IPaymentFactory
/// </summary>
/// <param name="svc"></param>
/// <returns></returns>
ValueTask<IPaymentProvider> CreateProvider(PaymentServices svc);
ValueTask<IPaymentProvider> CreateProvider(PaywallService svc);
}

View File

@ -1,11 +1,11 @@
using VoidCat.Model.Payments;
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Payment order store
/// </summary>
public interface IPaymentOrderStore : IBasicStore<PaymentOrder>
public interface IPaymentOrderStore : IBasicStore<PaywallOrder>
{
/// <summary>
/// Update the status of an order
@ -13,5 +13,5 @@ public interface IPaymentOrderStore : IBasicStore<PaymentOrder>
/// <param name="order"></param>
/// <param name="status"></param>
/// <returns></returns>
ValueTask UpdateStatus(Guid order, PaymentOrderStatus status);
ValueTask UpdateStatus(Guid order, PaywallOrderStatus status);
}

View File

@ -1,4 +1,4 @@
using VoidCat.Model.Payments;
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
@ -12,12 +12,12 @@ public interface IPaymentProvider
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
ValueTask<PaymentOrder?> CreateOrder(PaymentConfig file);
ValueTask<PaywallOrder?> CreateOrder(Paywall file);
/// <summary>
/// Get the status of an existing order with the provider
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<PaymentOrder?> GetOrderStatus(Guid id);
ValueTask<PaywallOrder?> GetOrderStatus(Guid id);
}

View File

@ -1,10 +1,10 @@
using VoidCat.Model.Payments;
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Store for payment configs
/// </summary>
public interface IPaymentStore : IBasicStore<PaymentConfig>
public interface IPaymentStore : IBasicStore<Paywall>
{
}

View File

@ -1,38 +0,0 @@
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Store interface where there is a public and private model
/// </summary>
/// <typeparam name="TPublic"></typeparam>
/// <typeparam name="TPrivate"></typeparam>
public interface IPublicPrivateStore<TPublic, TPrivate>
{
/// <summary>
/// Get the public model
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<TPublic?> Get(Guid id);
/// <summary>
/// Get the private model (contains sensitive data)
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<TPrivate?> GetPrivate(Guid id);
/// <summary>
/// Set the private obj in the store
/// </summary>
/// <param name="id"></param>
/// <param name="obj"></param>
/// <returns></returns>
ValueTask Set(Guid id, TPrivate obj);
/// <summary>
/// Delete the object from the store
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask Delete(Guid id);
}

View File

@ -1,4 +1,4 @@
using VoidCat.Model.User;
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;

View File

@ -1,20 +1,26 @@
using VoidCat.Model;
using VoidCat.Model.User;
using VoidCat.Database;
using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// User store
/// </summary>
public interface IUserStore : IPublicPrivateStore<User, InternalUser>
public interface IUserStore
{
/// <summary>
/// Get a single user
/// </summary>
/// <param name="id"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
ValueTask<T?> Get<T>(Guid id) where T : User;
ValueTask<User?> Get(Guid id);
/// <summary>
/// Add a new user to the store
/// </summary>
/// <param name="u"></param>
/// <returns></returns>
ValueTask Add(User u);
/// <summary>
/// Lookup a user by their email address
@ -28,14 +34,14 @@ public interface IUserStore : IPublicPrivateStore<User, InternalUser>
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
ValueTask<PagedResult<PrivateUser>> ListUsers(PagedRequest request);
ValueTask<PagedResult<User>> ListUsers(PagedRequest request);
/// <summary>
/// Update a users profile
/// </summary>
/// <param name="newUser"></param>
/// <returns></returns>
ValueTask UpdateProfile(PublicUser newUser);
ValueTask UpdateProfile(User newUser);
/// <summary>
/// Updates the last login timestamp for the user
@ -50,5 +56,12 @@ public interface IUserStore : IPublicPrivateStore<User, InternalUser>
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
ValueTask AdminUpdateUser(PrivateUser user);
ValueTask AdminUpdateUser(User user);
/// <summary>
/// Delete a user from the system
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask Delete(Guid id);
}

View File

@ -1,4 +1,4 @@
using VoidCat.Model;
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;

View File

@ -1,4 +1,4 @@
using VoidCat.Model;
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;

View File

@ -29,7 +29,7 @@ public sealed class DeleteExpiredFiles : BackgroundService
var fileInfoManager = scope.ServiceProvider.GetRequiredService<FileInfoManager>();
var fileStoreFactory = scope.ServiceProvider.GetRequiredService<FileStoreFactory>();
var files = await metadata.ListFiles<SecretFileMeta>(new(0, int.MaxValue));
var files = await metadata.ListFiles(new(0, int.MaxValue));
await foreach (var f in files.Results.WithCancellation(stoppingToken))
{
try

View File

@ -1,5 +1,5 @@
using VoidCat.Model;
using VoidCat.Model.User;
using VoidCat.Database;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;

View File

@ -29,8 +29,9 @@ public class VirusScannerService : BackgroundService
var page = 0;
while (true)
{
var files = await _fileStore.ListFiles<FileMeta>(new(page, 1_000));
var files = await _fileStore.ListFiles(new(page, 1_000));
if (files.Pages < page) break;
page++;
await foreach (var file in files.Results.WithCancellation(stoppingToken))
@ -46,8 +47,7 @@ public class VirusScannerService : BackgroundService
{
var result = await _scanner.ScanFile(file.Id, stoppingToken);
await _scanStore.Add(result.Id, result);
_logger.LogInformation("Scanned file {Id}, IsVirus = {Result}", result.File,
result.IsVirus);
_logger.LogInformation("Scanned file {Id}, IsVirus = {Result}", result.File, result.Score);
}
catch (RateLimitedException rx)
{
@ -66,4 +66,4 @@ public class VirusScannerService : BackgroundService
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
}

View File

@ -5,29 +5,29 @@ namespace VoidCat.Services;
/// <inheritdoc />
public abstract class BasicCacheStore<TStore> : IBasicStore<TStore>
{
protected readonly ICache _cache;
protected readonly ICache Cache;
protected BasicCacheStore(ICache cache)
{
_cache = cache;
Cache = cache;
}
/// <inheritdoc />
public virtual ValueTask<TStore?> Get(Guid id)
{
return _cache.Get<TStore>(MapKey(id));
return Cache.Get<TStore>(MapKey(id));
}
/// <inheritdoc />
public virtual ValueTask Add(Guid id, TStore obj)
{
return _cache.Set(MapKey(id), obj);
return Cache.Set(MapKey(id), obj);
}
/// <inheritdoc />
public virtual ValueTask Delete(Guid id)
{
return _cache.Delete(MapKey(id));
return Cache.Delete(MapKey(id));
}
/// <summary>

View File

@ -1,5 +1,6 @@
using System.Collections.Immutable;
using VoidCat.Database;
using VoidCat.Model;
using VoidCat.Model.User;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Files;
@ -32,36 +33,47 @@ public sealed class FileInfoManager
/// Get all metadata for a single file
/// </summary>
/// <param name="id"></param>
/// <param name="withEditSecret"></param>
/// <returns></returns>
public ValueTask<PublicVoidFile?> Get(Guid id)
public async ValueTask<VoidFileResponse?> Get(Guid id, bool withEditSecret)
{
return Get<PublicVoidFile, FileMeta>(id);
}
var meta = await _metadataStore.Get(id);
if (meta == default) return default;
/// <summary>
/// Get all private metadata for a single file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ValueTask<PrivateVoidFile?> GetPrivate(Guid id)
{
return Get<PrivateVoidFile, SecretFileMeta>(id);
var payment = await _paymentStore.Get(id);
var bandwidth = await _statsReporter.GetBandwidth(id);
var virusScan = await _virusScanStore.GetByFile(id);
var uploader = await _userUploadsStore.Uploader(id);
var user = uploader.HasValue ? await _userStore.Get(uploader.Value) : null;
return new VoidFileResponse
{
Id = id,
Metadata = meta.ToMeta(withEditSecret),
Payment = payment,
Bandwidth = bandwidth,
Uploader = user?.Flags.HasFlag(UserFlags.PublicProfile) == true || withEditSecret ? user?.ToApiUser(false) : null,
VirusScan = virusScan?.ToVirusStatus()
};
}
/// <summary>
/// Get all metadata for multiple files
/// </summary>
/// <param name="ids"></param>
/// <param name="withEditSecret"></param>
/// <returns></returns>
public async ValueTask<IReadOnlyList<PublicVoidFile>> Get(Guid[] ids)
public async ValueTask<IReadOnlyList<VoidFileResponse>> Get(Guid[] ids, bool withEditSecret)
{
var ret = new List<PublicVoidFile>();
foreach (var id in ids)
//todo: improve this
var ret = new List<VoidFileResponse>();
foreach (var i in ids)
{
var v = await Get(id);
if (v != default)
var x = await Get(i, withEditSecret);
if (x != default)
{
ret.Add(v);
ret.Add(x);
}
}
@ -80,28 +92,4 @@ public sealed class FileInfoManager
await _statsReporter.Delete(id);
await _virusScanStore.Delete(id);
}
private async ValueTask<TFile?> Get<TFile, TMeta>(Guid id)
where TMeta : FileMeta where TFile : VoidFile<TMeta>, new()
{
var meta = _metadataStore.Get<TMeta>(id);
var payment = _paymentStore.Get(id);
var bandwidth = _statsReporter.GetBandwidth(id);
var virusScan = _virusScanStore.GetByFile(id);
var uploader = _userUploadsStore.Uploader(id);
await Task.WhenAll(meta.AsTask(), payment.AsTask(), bandwidth.AsTask(), virusScan.AsTask(), uploader.AsTask());
if (meta.Result == default) return default;
var user = uploader.Result.HasValue ? await _userStore.Get<PublicUser>(uploader.Result.Value) : null;
return new TFile()
{
Id = id,
Metadata = meta.Result,
Payment = payment.Result,
Bandwidth = bandwidth.Result,
Uploader = user?.Flags.HasFlag(UserFlags.PublicProfile) == true ? user : null,
VirusScan = virusScan.Result
};
}
}
}

View File

@ -11,23 +11,23 @@ public static class FileStorageStartup
services.AddTransient<FileInfoManager>();
services.AddTransient<FileStoreFactory>();
services.AddTransient<CompressContent>();
if (settings.CloudStorage != default)
{
// S3 storage
foreach (var s3 in settings.CloudStorage.S3 ?? Array.Empty<S3BlobConfig>())
{
services.AddTransient<IFileStore>((svc) =>
new S3FileStore(s3,
svc.GetRequiredService<IAggregateStatsCollector>(),
svc.GetRequiredService<FileInfoManager>(),
svc.GetRequiredService<ICache>()));
if (settings.MetadataStore == s3.Name)
{
services.AddSingleton<IFileMetadataStore>((svc) =>
new S3FileMetadataStore(s3, svc.GetRequiredService<ILogger<S3FileMetadataStore>>()));
}
services.AddTransient<IFileStore>((svc) =>
new S3FileStore(s3,
svc.GetRequiredService<IAggregateStatsCollector>(),
svc.GetRequiredService<IFileMetadataStore>(),
svc.GetRequiredService<ICache>()));
}
}
@ -37,7 +37,7 @@ public static class FileStorageStartup
services.AddTransient<IFileStore, LocalDiskFileStore>();
if (settings.MetadataStore is "postgres" or "local-disk")
{
services.AddSingleton<IFileMetadataStore, PostgresFileMetadataStore>();
services.AddTransient<IFileMetadataStore, PostgresFileMetadataStore>();
}
}
else

View File

@ -36,7 +36,7 @@ public class FileStoreFactory : IFileStore
public string? Key => null;
/// <inheritdoc />
public ValueTask<PrivateVoidFile> Ingress(IngressPayload payload, CancellationToken cts)
public ValueTask<Database.File> Ingress(IngressPayload payload, CancellationToken cts)
{
var store = GetFileStore(payload.Meta.Storage!);
if (store == default)

View File

@ -22,18 +22,18 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
}
/// <inheritdoc />
public ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : FileMeta
public ValueTask<Database.File?> Get(Guid id)
{
return GetMeta<TMeta>(id);
return GetMeta<Database.File>(id);
}
/// <inheritdoc />
public async ValueTask<IReadOnlyList<TMeta>> Get<TMeta>(Guid[] ids) where TMeta : FileMeta
public async ValueTask<IReadOnlyList<Database.File>> Get(Guid[] ids)
{
var ret = new List<TMeta>();
var ret = new List<Database.File>();
foreach (var id in ids)
{
var r = await GetMeta<TMeta>(id);
var r = await GetMeta<Database.File>(id);
if (r != null)
{
ret.Add(r);
@ -42,11 +42,16 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
return ret;
}
public ValueTask Add(Database.File f)
{
return Set(f.Id, f);
}
/// <inheritdoc />
public async ValueTask Update<TMeta>(Guid id, TMeta meta) where TMeta : FileMeta
public async ValueTask Update(Guid id, Database.File meta)
{
var oldMeta = await Get<SecretFileMeta>(id);
var oldMeta = await Get(id);
if (oldMeta == default) return;
oldMeta.Patch(meta);
@ -54,22 +59,18 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
}
/// <inheritdoc />
public ValueTask<PagedResult<TMeta>> ListFiles<TMeta>(PagedRequest request) where TMeta : FileMeta
public ValueTask<PagedResult<Database.File>> ListFiles(PagedRequest request)
{
async IAsyncEnumerable<TMeta> EnumerateFiles()
async IAsyncEnumerable<Database.File> EnumerateFiles()
{
foreach (var metaFile in
Directory.EnumerateFiles(Path.Join(_settings.DataDirectory, MetadataDir), "*.json"))
{
var json = await File.ReadAllTextAsync(metaFile);
var meta = JsonConvert.DeserializeObject<TMeta>(json);
var meta = JsonConvert.DeserializeObject<Database.File>(json);
if (meta != null)
{
yield return meta with
{
// TODO: remove after migration decay
Id = Guid.Parse(Path.GetFileNameWithoutExtension(metaFile))
};
yield return meta;
}
}
}
@ -86,7 +87,7 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
_ => results
};
return ValueTask.FromResult(new PagedResult<TMeta>
return ValueTask.FromResult(new PagedResult<Database.File>
{
Page = request.Page,
PageSize = request.PageSize,
@ -97,26 +98,14 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
/// <inheritdoc />
public async ValueTask<IFileMetadataStore.StoreStats> Stats()
{
var files = await ListFiles<FileMeta>(new(0, Int32.MaxValue));
var files = await ListFiles(new(0, Int32.MaxValue));
var count = await files.Results.CountAsync();
var size = await files.Results.SumAsync(a => (long) a.Size);
return new(count, (ulong) size);
}
/// <inheritdoc />
public ValueTask<FileMeta?> Get(Guid id)
{
return GetMeta<FileMeta>(id);
}
/// <inheritdoc />
public ValueTask<SecretFileMeta?> GetPrivate(Guid id)
{
return GetMeta<SecretFileMeta>(id);
}
/// <inheritdoc />
public async ValueTask Set(Guid id, SecretFileMeta meta)
public async ValueTask Set(Guid id, Database.File meta)
{
var path = MapMeta(id);
var json = JsonConvert.SerializeObject(meta);

View File

@ -42,7 +42,7 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
public string Key => "local-disk";
/// <inheritdoc />
public async ValueTask<PrivateVoidFile> Ingress(IngressPayload payload, CancellationToken cts)
public async ValueTask<Database.File> Ingress(IngressPayload payload, CancellationToken cts)
{
var finalPath = MapPath(payload.Id);
await using var fsTemp = new FileStream(finalPath,
@ -53,7 +53,7 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
if (payload.ShouldStripMetadata && payload.Segment == payload.TotalSegments)
{
fsTemp.Close();
var ext = Path.GetExtension(vf.Metadata!.Name);
var ext = Path.GetExtension(vf.Name);
var srcPath = $"{finalPath}_orig{ext}";
File.Move(finalPath, srcPath);
@ -69,12 +69,9 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
var hash = await SHA256.Create().ComputeHashAsync(fInfo.OpenRead(), cts);
vf = vf with
{
Metadata = vf.Metadata! with
{
Size = (ulong)fInfo.Length,
Digest = hash.ToHex(),
MimeType = res.MimeType ?? vf.Metadata.MimeType
}
Size = (ulong)fInfo.Length,
Digest = hash.ToHex(),
MimeType = res.MimeType ?? vf.MimeType
};
}
else
@ -86,7 +83,7 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
if (payload.Segment == payload.TotalSegments)
{
var t = await vf.Metadata!.MakeTorrent(
var t = await vf.ToMeta(false).MakeTorrent(vf.Id,
new FileStream(finalPath, FileMode.Open),
_settings.SiteUrl,
_settings.TorrentTrackers);
@ -94,7 +91,7 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
var ub = new UriBuilder(_settings.SiteUrl);
ub.Path = $"/d/{vf.Id.ToBase58()}.torrent";
vf.Metadata!.MagnetLink = $"{t.GetMagnetLink()}&xs={Uri.EscapeDataString(ub.ToString())}";
vf.MagnetLink = $"{t.GetMagnetLink()}&xs={Uri.EscapeDataString(ub.ToString())}";
}
return vf;

View File

@ -1,4 +1,4 @@
using Dapper;
using Microsoft.EntityFrameworkCore;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
@ -7,119 +7,103 @@ namespace VoidCat.Services.Files;
/// <inheritdoc />
public class PostgresFileMetadataStore : IFileMetadataStore
{
private readonly PostgresConnectionFactory _connection;
private readonly VoidContext _db;
private readonly IServiceScopeFactory _scopeFactory;
public PostgresFileMetadataStore(PostgresConnectionFactory connection)
public PostgresFileMetadataStore(VoidContext db, IServiceScopeFactory scopeFactory)
{
_connection = connection;
_db = db;
_scopeFactory = scopeFactory;
}
/// <inheritdoc />
public string? Key => "postgres";
/// <inheritdoc />
public ValueTask<FileMeta?> Get(Guid id)
{
return Get<FileMeta>(id);
}
/// <inheritdoc />
public ValueTask<SecretFileMeta?> GetPrivate(Guid id)
public async ValueTask<Database.File?> Get(Guid id)
{
return Get<SecretFileMeta>(id);
return await _db.Files
.AsNoTracking()
.Include(a => a.Paywall)
.SingleOrDefaultAsync(a => a.Id == id);
}
/// <inheritdoc />
public async ValueTask Set(Guid id, SecretFileMeta obj)
public async ValueTask Add(Database.File f)
{
await using var conn = await _connection.Get();
await conn.ExecuteAsync(
@"insert into
""Files""(""Id"", ""Name"", ""Size"", ""Uploaded"", ""Description"", ""MimeType"", ""Digest"", ""EditSecret"", ""Expires"", ""Storage"", ""EncryptionParams"", ""MagnetLink"")
values(:id, :name, :size, :uploaded, :description, :mimeType, :digest, :editSecret, :expires, :store, :encryptionParams, :magnetLink)
on conflict (""Id"") do update set
""Name"" = :name,
""Size"" = :size,
""Description"" = :description,
""MimeType"" = :mimeType,
""Expires"" = :expires,
""Storage"" = :store,
""EncryptionParams"" = :encryptionParams,
""MagnetLink"" = :magnetLink",
new
{
id,
name = obj.Name,
size = (long) obj.Size,
uploaded = obj.Uploaded.ToUniversalTime(),
description = obj.Description,
mimeType = obj.MimeType,
digest = obj.Digest,
editSecret = obj.EditSecret,
expires = obj.Expires?.ToUniversalTime(),
store = obj.Storage,
encryptionParams = obj.EncryptionParams,
magnetLink = obj.MagnetLink,
});
_db.Files.Add(f);
await _db.SaveChangesAsync();
}
/// <inheritdoc />
public async ValueTask Delete(Guid id)
{
await using var conn = await _connection.Get();
await conn.ExecuteAsync("delete from \"Files\" where \"Id\" = :id", new {id});
await _db.Files
.Where(a => a.Id == id)
.ExecuteDeleteAsync();
}
/// <inheritdoc />
public async ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : FileMeta
public async ValueTask<IReadOnlyList<Database.File>> Get(Guid[] ids)
{
await using var conn = await _connection.Get();
return await conn.QuerySingleOrDefaultAsync<TMeta?>(@"select * from ""Files"" where ""Id"" = :id",
new {id});
return await _db.Files
.Include(a => a.Paywall)
.Where(a => ids.Contains(a.Id))
.ToArrayAsync();
}
/// <inheritdoc />
public async ValueTask<IReadOnlyList<TMeta>> Get<TMeta>(Guid[] ids) where TMeta : FileMeta
public async ValueTask Update(Guid id, Database.File obj)
{
await using var conn = await _connection.Get();
var ret = await conn.QueryAsync<TMeta>("select * from \"Files\" where \"Id\" in :ids", new {ids});
return ret.ToList();
}
/// <inheritdoc />
public async ValueTask Update<TMeta>(Guid id, TMeta meta) where TMeta : FileMeta
{
var oldMeta = await Get<SecretFileMeta>(id);
if (oldMeta == default) return;
oldMeta.Patch(meta);
await Set(id, oldMeta);
}
/// <inheritdoc />
public async ValueTask<PagedResult<TMeta>> ListFiles<TMeta>(PagedRequest request) where TMeta : FileMeta
{
await using var conn = await _connection.Get();
var count = await conn.ExecuteScalarAsync<int>(@"select count(*) from ""Files""");
async IAsyncEnumerable<TMeta> Enumerate()
var existing = await _db.Files.FindAsync(id);
if (existing == default)
{
var orderBy = request.SortBy switch
{
PagedSortBy.Date => "Uploaded",
PagedSortBy.Name => "Name",
PagedSortBy.Size => "Size",
_ => "Id"
};
await using var iconn = await _connection.Get();
var orderDirection = request.SortOrder == PageSortOrder.Asc ? "asc" : "desc";
var results = await iconn.QueryAsync<TMeta>(
$"select * from \"Files\" order by \"{orderBy}\" {orderDirection} offset @offset limit @limit",
new {offset = request.PageSize * request.Page, limit = request.PageSize});
return;
}
foreach (var meta in results)
existing.Patch(obj);
await _db.SaveChangesAsync();
}
/// <inheritdoc />
public async ValueTask<PagedResult<Database.File>> ListFiles(PagedRequest request)
{
var count = await _db.Files.CountAsync();
async IAsyncEnumerable<Database.File> Enumerate()
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<VoidContext>();
var q = db.Files.AsNoTracking().AsQueryable();
switch (request.SortBy, request.SortOrder)
{
yield return meta;
case (PagedSortBy.Id, PageSortOrder.Asc):
q = q.OrderBy(a => a.Id);
break;
case (PagedSortBy.Id, PageSortOrder.Dsc):
q = q.OrderByDescending(a => a.Id);
break;
case (PagedSortBy.Name, PageSortOrder.Asc):
q = q.OrderBy(a => a.Name);
break;
case (PagedSortBy.Name, PageSortOrder.Dsc):
q = q.OrderByDescending(a => a.Name);
break;
case (PagedSortBy.Date, PageSortOrder.Asc):
q = q.OrderBy(a => a.Uploaded);
break;
case (PagedSortBy.Date, PageSortOrder.Dsc):
q = q.OrderByDescending(a => a.Uploaded);
break;
case (PagedSortBy.Size, PageSortOrder.Asc):
q = q.OrderBy(a => a.Size);
break;
case (PagedSortBy.Size, PageSortOrder.Dsc):
q = q.OrderByDescending(a => a.Size);
break;
}
await foreach (var r in q.Skip(request.Page * request.PageSize).Take(request.PageSize).AsAsyncEnumerable())
{
yield return r;
}
}
@ -135,9 +119,11 @@ on conflict (""Id"") do update set
/// <inheritdoc />
public async ValueTask<IFileMetadataStore.StoreStats> Stats()
{
await using var conn = await _connection.Get();
var v = await conn.QuerySingleAsync<(long Files, long Size)>(
@"select count(1) ""Files"", cast(sum(""Size"") as bigint) ""Size"" from ""Files""");
return new(v.Files, (ulong) v.Size);
var size = await _db.Files
.AsNoTracking()
.SumAsync(a => (long)a.Size);
var count = await _db.Files.CountAsync();
return new(count, (ulong)size);
}
}
}

View File

@ -19,22 +19,35 @@ public class S3FileMetadataStore : IFileMetadataStore
_client = _config.CreateClient();
}
/// <inheritdoc />
public string? Key => _config.Name;
/// <inheritdoc />
public ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : FileMeta
public async ValueTask<Database.File?> Get(Guid id)
{
return GetMeta<TMeta>(id);
try
{
var obj = await _client.GetObjectAsync(_config.BucketName, ToKey(id));
using var sr = new StreamReader(obj.ResponseStream);
var json = await sr.ReadToEndAsync();
var ret = JsonConvert.DeserializeObject<Database.File>(json);
return ret;
}
catch (AmazonS3Exception aex)
{
_logger.LogError(aex, "Failed to get metadata for {Id}, {Error}", id, aex.Message);
}
return default;
}
/// <inheritdoc />
public async ValueTask<IReadOnlyList<TMeta>> Get<TMeta>(Guid[] ids) where TMeta : FileMeta
public async ValueTask<IReadOnlyList<Database.File>> Get(Guid[] ids)
{
var ret = new List<TMeta>();
var ret = new List<Database.File>();
foreach (var id in ids)
{
var r = await GetMeta<TMeta>(id);
var r = await Get(id);
if (r != null)
{
ret.Add(r);
@ -43,11 +56,15 @@ public class S3FileMetadataStore : IFileMetadataStore
return ret;
}
public ValueTask Add(Database.File f)
{
return Set(f.Id, f);
}
/// <inheritdoc />
public async ValueTask Update<TMeta>(Guid id, TMeta meta) where TMeta : FileMeta
public async ValueTask Update(Guid id, Database.File meta)
{
var oldMeta = await Get<SecretFileMeta>(id);
var oldMeta = await Get(id);
if (oldMeta == default) return;
oldMeta.Patch(meta);
@ -55,9 +72,9 @@ public class S3FileMetadataStore : IFileMetadataStore
}
/// <inheritdoc />
public ValueTask<PagedResult<TMeta>> ListFiles<TMeta>(PagedRequest request) where TMeta : FileMeta
public ValueTask<PagedResult<Database.File>> ListFiles(PagedRequest request)
{
async IAsyncEnumerable<TMeta> Enumerate()
async IAsyncEnumerable<Database.File> Enumerate()
{
var obj = await _client.ListObjectsV2Async(new()
{
@ -70,7 +87,7 @@ public class S3FileMetadataStore : IFileMetadataStore
{
if (Guid.TryParse(file.Key.Split("metadata_")[1], out var id))
{
var meta = await GetMeta<TMeta>(id);
var meta = await Get(id);
if (meta != default)
{
yield return meta;
@ -79,7 +96,7 @@ public class S3FileMetadataStore : IFileMetadataStore
}
}
return ValueTask.FromResult(new PagedResult<TMeta>
return ValueTask.FromResult(new PagedResult<Database.File>
{
Page = request.Page,
PageSize = request.PageSize,
@ -90,26 +107,19 @@ public class S3FileMetadataStore : IFileMetadataStore
/// <inheritdoc />
public async ValueTask<IFileMetadataStore.StoreStats> Stats()
{
var files = await ListFiles<FileMeta>(new(0, Int32.MaxValue));
var files = await ListFiles(new(0, Int32.MaxValue));
var count = await files.Results.CountAsync();
var size = await files.Results.SumAsync(a => (long) a.Size);
return new(count, (ulong) size);
}
/// <inheritdoc />
public ValueTask<FileMeta?> Get(Guid id)
public async ValueTask Delete(Guid id)
{
return GetMeta<FileMeta>(id);
await _client.DeleteObjectAsync(_config.BucketName, ToKey(id));
}
/// <inheritdoc />
public ValueTask<SecretFileMeta?> GetPrivate(Guid id)
{
return GetMeta<SecretFileMeta>(id);
}
/// <inheritdoc />
public async ValueTask Set(Guid id, SecretFileMeta meta)
private async ValueTask Set(Guid id, Database.File meta)
{
await _client.PutObjectAsync(new()
{
@ -120,35 +130,5 @@ public class S3FileMetadataStore : IFileMetadataStore
});
}
/// <inheritdoc />
public async ValueTask Delete(Guid id)
{
await _client.DeleteObjectAsync(_config.BucketName, ToKey(id));
}
private async ValueTask<TMeta?> GetMeta<TMeta>(Guid id) where TMeta : FileMeta
{
try
{
var obj = await _client.GetObjectAsync(_config.BucketName, ToKey(id));
using var sr = new StreamReader(obj.ResponseStream);
var json = await sr.ReadToEndAsync();
var ret = JsonConvert.DeserializeObject<TMeta>(json);
if (ret != default)
{
ret.Id = id;
}
return ret;
}
catch (AmazonS3Exception aex)
{
_logger.LogError(aex, "Failed to get metadata for {Id}, {Error}", id, aex.Message);
}
return default;
}
private static string ToKey(Guid id) => $"metadata_{id}";
}

View File

@ -9,13 +9,13 @@ namespace VoidCat.Services.Files;
/// <inheritdoc cref="VoidCat.Services.Abstractions.IFileStore" />
public class S3FileStore : StreamFileStore, IFileStore
{
private readonly FileInfoManager _fileInfo;
private readonly IFileMetadataStore _fileInfo;
private readonly AmazonS3Client _client;
private readonly S3BlobConfig _config;
private readonly IAggregateStatsCollector _statsCollector;
private readonly ICache _cache;
public S3FileStore(S3BlobConfig settings, IAggregateStatsCollector stats, FileInfoManager fileInfo, ICache cache) : base(stats)
public S3FileStore(S3BlobConfig settings, IAggregateStatsCollector stats, IFileMetadataStore fileInfo, ICache cache) : base(stats)
{
_fileInfo = fileInfo;
_cache = cache;
@ -28,7 +28,7 @@ public class S3FileStore : StreamFileStore, IFileStore
public string Key => _config.Name;
/// <inheritdoc />
public async ValueTask<PrivateVoidFile> Ingress(IngressPayload payload, CancellationToken cts)
public async ValueTask<Database.File> Ingress(IngressPayload payload, CancellationToken cts)
{
if (payload.IsMultipart) return await IngressMultipart(payload, cts);
@ -75,15 +75,15 @@ public class S3FileStore : StreamFileStore, IFileStore
Key = request.Id.ToString(),
ResponseHeaderOverrides = new()
{
ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"",
ContentType = meta?.Metadata?.MimeType
ContentDisposition = $"inline; filename=\"{meta?.Name}\"",
ContentType = meta?.MimeType
}
});
return new(new Uri(url));
}
public async ValueTask<PagedResult<PublicVoidFile>> ListFiles(PagedRequest request)
public async ValueTask<PagedResult<Database.File>> ListFiles(PagedRequest request)
{
try
{
@ -103,7 +103,7 @@ public class S3FileStore : StreamFileStore, IFileStore
_ => objs.S3Objects.AsEnumerable()
};
async IAsyncEnumerable<PublicVoidFile> EnumerateFiles(IEnumerable<S3Object> page)
async IAsyncEnumerable<Database.File> EnumerateFiles(IEnumerable<S3Object> page)
{
foreach (var item in page)
{
@ -133,7 +133,7 @@ public class S3FileStore : StreamFileStore, IFileStore
Page = request.Page,
PageSize = request.PageSize,
TotalResults = 0,
Results = AsyncEnumerable.Empty<PublicVoidFile>()
Results = AsyncEnumerable.Empty<Database.File>()
};
}
}
@ -163,7 +163,7 @@ public class S3FileStore : StreamFileStore, IFileStore
return obj.ResponseStream;
}
private async Task<PrivateVoidFile> IngressMultipart(IngressPayload payload, CancellationToken cts)
private async Task<Database.File> IngressMultipart(IngressPayload payload, CancellationToken cts)
{
string? uploadId = null;
var cacheKey = $"s3:{_config.Name}:multipart-upload-id:{payload.Id}";

View File

@ -31,7 +31,7 @@ public abstract class StreamFileStore
}
}
protected async ValueTask<PrivateVoidFile> IngressToStream(Stream outStream, IngressPayload payload,
protected async ValueTask<Database.File> IngressToStream(Stream outStream, IngressPayload payload,
CancellationToken cts)
{
var id = payload.Id;
@ -48,13 +48,14 @@ public abstract class StreamFileStore
return HandleCompletedUpload(payload, total);
}
protected PrivateVoidFile HandleCompletedUpload(IngressPayload payload, ulong totalSize)
protected Database.File HandleCompletedUpload(IngressPayload payload, ulong totalSize)
{
var meta = payload.Meta;
if (payload.IsAppend)
{
meta = meta with
{
Id = payload.Id,
Size = meta.Size + totalSize
};
}
@ -62,19 +63,14 @@ public abstract class StreamFileStore
{
meta = meta with
{
Uploaded = DateTimeOffset.UtcNow,
Id = payload.Id,
Uploaded = DateTime.UtcNow,
EditSecret = Guid.NewGuid(),
Size = totalSize
};
}
var vf = new PrivateVoidFile()
{
Id = payload.Id,
Metadata = meta
};
return vf;
return meta;
}
private async Task<ulong> IngressInternal(Guid id, Stream ingress, Stream outStream,

View File

@ -1,100 +0,0 @@
using System.Data;
using FluentMigrator;
using VoidCat.Model.User;
namespace VoidCat.Services.Migrations.Database;
[Migration(20220604_2232)]
public class Init : Migration
{
public override void Up()
{
Create.Table("Users")
.WithColumn("Id").AsGuid().PrimaryKey()
.WithColumn("Email").AsString().Indexed()
.WithColumn("Password").AsString()
.WithColumn("Created").AsDateTimeOffset().WithDefault(SystemMethods.CurrentUTCDateTime)
.WithColumn("LastLogin").AsDateTimeOffset().Nullable()
.WithColumn("Avatar").AsString().Nullable()
.WithColumn("DisplayName").AsString().WithDefaultValue("void user")
.WithColumn("Flags").AsInt32().WithDefaultValue((int) UserFlags.PublicProfile);
Create.Table("Files")
.WithColumn("Id").AsGuid().PrimaryKey()
.WithColumn("Name").AsString().Nullable()
.WithColumn("Size").AsInt64()
.WithColumn("Uploaded").AsDateTimeOffset().Indexed().WithDefault(SystemMethods.CurrentUTCDateTime)
.WithColumn("Description").AsString().Nullable()
.WithColumn("MimeType").AsString().WithDefaultValue("application/octet-stream")
.WithColumn("Digest").AsString().Nullable()
.WithColumn("EditSecret").AsGuid();
Create.Table("UserFiles")
.WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).Indexed()
.WithColumn("User").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade).Indexed();
Create.UniqueConstraint()
.OnTable("UserFiles")
.Columns("File", "User");
Create.Table("Paywall")
.WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).PrimaryKey()
.WithColumn("Service").AsInt16()
.WithColumn("Currency").AsInt16()
.WithColumn("Amount").AsDecimal();
Create.Table("PaywallStrike")
.WithColumn("File").AsGuid().ForeignKey("Paywall", "File").OnDelete(Rule.Cascade).PrimaryKey()
.WithColumn("Handle").AsString();
Create.Table("PaywallOrder")
.WithColumn("Id").AsGuid().PrimaryKey()
.WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).Indexed()
.WithColumn("Service").AsInt16()
.WithColumn("Currency").AsInt16()
.WithColumn("Amount").AsDecimal()
.WithColumn("Status").AsInt16().Indexed();
Create.Table("PaywallOrderLightning")
.WithColumn("Order").AsGuid().ForeignKey("PaywallOrder", "Id").OnDelete(Rule.Cascade).PrimaryKey()
.WithColumn("Invoice").AsString()
.WithColumn("Expire").AsDateTimeOffset();
Create.Table("UserRoles")
.WithColumn("User").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade).Indexed()
.WithColumn("Role").AsString().NotNullable();
Create.UniqueConstraint()
.OnTable("UserRoles")
.Columns("User", "Role");
Create.Table("EmailVerification")
.WithColumn("User").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade)
.WithColumn("Code").AsGuid()
.WithColumn("Expires").AsDateTimeOffset();
Create.UniqueConstraint()
.OnTable("EmailVerification")
.Columns("User", "Code");
Create.Table("VirusScanResult")
.WithColumn("Id").AsGuid().PrimaryKey()
.WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).Indexed()
.WithColumn("ScanTime").AsDateTimeOffset().WithDefault(SystemMethods.CurrentUTCDateTime)
.WithColumn("Scanner").AsString()
.WithColumn("Score").AsDecimal()
.WithColumn("Names").AsString().Nullable();
}
public override void Down()
{
Delete.Table("Users");
Delete.Table("Files");
Delete.Table("UsersFiles");
Delete.Table("Paywall");
Delete.Table("PaywallStrike");
Delete.Table("UserRoles");
Delete.Table("EmailVerification");
Delete.Table("VirusScanResult");
}
}

View File

@ -1,19 +0,0 @@
using FluentMigrator;
namespace VoidCat.Services.Migrations.Database;
[Migration(20220615_2238)]
public class FileExpiry : Migration {
public override void Up()
{
Create.Column("Expires")
.OnTable("Files")
.AsDateTimeOffset().Nullable().Indexed();
}
public override void Down()
{
Delete.Column("Expires")
.FromTable("Files");
}
}

View File

@ -1,37 +0,0 @@
using System.Data;
using FluentMigrator;
namespace VoidCat.Services.Migrations.Database;
[Migration(20220725_1137)]
public class MinorVersion1 : Migration
{
public override void Up()
{
Create.Table("ApiKey")
.WithColumn("Id").AsGuid().PrimaryKey()
.WithColumn("UserId").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade).Indexed()
.WithColumn("Token").AsString()
.WithColumn("Expiry").AsDateTimeOffset()
.WithColumn("Created").AsDateTimeOffset().WithDefault(SystemMethods.CurrentUTCDateTime);
Create.Column("Storage")
.OnTable("Files")
.AsString().WithDefaultValue("local-disk");
Create.Column("Storage")
.OnTable("Users")
.AsString().WithDefaultValue("local-disk");
}
public override void Down()
{
Delete.Table("ApiKey");
Delete.Column("Storage")
.FromTable("Files");
Delete.Column("Storage")
.FromTable("Users");
}
}

View File

@ -1,27 +0,0 @@
using FluentMigrator;
namespace VoidCat.Services.Migrations.Database;
[Migration(20220908_1527)]
public class PaywallToPayments : Migration
{
public override void Up()
{
Rename.Table("Paywall")
.To("Payment");
Rename.Table("PaywallOrder")
.To("PaymentOrder");
Rename.Table("PaywallStrike")
.To("PaymentStrike");
Rename.Table("PaywallOrderLightning")
.To("PaymentOrderLightning");
}
public override void Down()
{
// yolo
}
}

View File

@ -1,20 +0,0 @@
using FluentMigrator;
namespace VoidCat.Services.Migrations.Database;
[Migration(20220908_1602)]
public class OptionalPayments : Migration{
public override void Up()
{
Create.Column("Required")
.OnTable("Payment")
.AsBoolean()
.WithDefaultValue(true);
}
public override void Down()
{
Delete.Column("Required")
.FromTable("Payment");
}
}

View File

@ -1,44 +0,0 @@
using System.Data;
using FluentMigrator;
namespace VoidCat.Services.Migrations.Database;
[Migration(20220907_2015)]
public class AccountTypes : Migration
{
public override void Up()
{
Create.Column("AuthType")
.OnTable("Users")
.AsInt16()
.WithDefaultValue(0);
Alter.Column("Password")
.OnTable("Users")
.AsString()
.Nullable();
Create.Table("UsersAuthToken")
.WithColumn("Id").AsGuid().PrimaryKey()
.WithColumn("User").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade).Indexed()
.WithColumn("Provider").AsString()
.WithColumn("AccessToken").AsString()
.WithColumn("TokenType").AsString()
.WithColumn("Expires").AsDateTimeOffset()
.WithColumn("RefreshToken").AsString()
.WithColumn("Scope").AsString();
}
public override void Down()
{
Delete.Column("Type")
.FromTable("Users");
Alter.Column("Password")
.OnTable("Users")
.AsString()
.NotNullable();
Delete.Table("UsersAuthToken");
}
}

View File

@ -1,20 +0,0 @@
using FluentMigrator;
namespace VoidCat.Services.Migrations.Database;
[Migration(20220911_1635)]
public class EncryptionParams : Migration{
public override void Up()
{
Create.Column("EncryptionParams")
.OnTable("Files")
.AsString()
.Nullable();
}
public override void Down()
{
Delete.Column("EncryptionParams")
.FromTable("Files");
}
}

View File

@ -1,20 +0,0 @@
using FluentMigrator;
namespace VoidCat.Services.Migrations.Database;
[Migration(20230304_1509)]
public class MagnetLink : Migration {
public override void Up()
{
Create.Column("MagnetLink")
.OnTable("Files")
.AsString()
.Nullable();
}
public override void Down()
{
Delete.Column("MagnetLink")
.FromTable("Files");
}
}

View File

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
namespace VoidCat.Services.Migrations;
public class EFMigration : IMigration
{
private readonly VoidContext _db;
public EFMigration(VoidContext db)
{
_db = db;
}
public int Order => 0;
public async ValueTask<IMigration.MigrationResult> Migrate(string[] args)
{
await _db.Database.MigrateAsync();
return IMigration.MigrationResult.Completed;
}
}

View File

@ -0,0 +1,88 @@
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using VoidCat.Model;
namespace VoidCat.Services.Migrations;
public class EFMigrationSetup : IMigration
{
private readonly VoidContext _db;
private readonly VoidSettings _settings;
public EFMigrationSetup(VoidContext db, VoidSettings settings)
{
_db = db;
_settings = settings;
}
public int Order => -99;
public async ValueTask<IMigration.MigrationResult> Migrate(string[] args)
{
if (!_settings.HasPostgres()) return IMigration.MigrationResult.Skipped;
var conn = (_db.Database.GetDbConnection() as NpgsqlConnection)!;
await conn.OpenAsync();
try
{
await using var cmd = new NpgsqlCommand("select max(\"Version\") from \"VersionInfo\"", conn);
var vMax = await cmd.ExecuteScalarAsync() as long?;
if (!(vMax > 0)) return IMigration.MigrationResult.Skipped;
await PrepEfMigration(conn);
}
catch (DbException dx) when (dx.SqlState is "42P01")
{
//ignored, VersionInfo does not exist
return IMigration.MigrationResult.Skipped;
}
return IMigration.MigrationResult.Completed;
}
private static async Task PrepEfMigration(NpgsqlConnection conn)
{
await using var tx = await conn.BeginTransactionAsync();
await new NpgsqlCommand(@"
ALTER TABLE ""Files"" ALTER COLUMN ""Size"" TYPE numeric(20) USING ""Size""::numeric;
ALTER TABLE ""Payment"" RENAME COLUMN ""File"" TO ""FileId"";
ALTER TABLE ""UserFiles"" RENAME COLUMN ""File"" TO ""FileId"";
ALTER TABLE ""UserFiles"" RENAME COLUMN ""User"" TO ""UserId"";
ALTER TABLE ""UserRoles"" RENAME COLUMN ""User"" TO ""UserId"";
ALTER TABLE ""UsersAuthToken"" RENAME COLUMN ""User"" TO ""UserId"";
ALTER TABLE ""UsersAuthToken"" ADD ""IdToken"" text NULL;
ALTER TABLE ""VirusScanResult"" RENAME COLUMN ""File"" TO ""FileId"";
ALTER TABLE ""ApiKey"" RENAME CONSTRAINT ""FK_ApiKey_UserId_Users_Id"" TO ""FK_ApiKey_Users_UserId"";
ALTER TABLE ""UserFiles"" RENAME CONSTRAINT ""FK_UserFiles_File_Files_Id"" TO ""FK_UserFiles_Files_FileId"";
ALTER TABLE ""UserFiles"" RENAME CONSTRAINT ""FK_UserFiles_User_Users_Id"" TO ""FK_UserFiles_Users_UserId"";
ALTER TABLE ""UserRoles"" RENAME CONSTRAINT ""FK_UserRoles_User_Users_Id"" TO ""FK_UserRoles_Users_UserId"";
ALTER TABLE ""UsersAuthToken"" RENAME CONSTRAINT ""FK_UsersAuthToken_User_Users_Id"" TO ""FK_UsersAuthToken_Users_UserId"";
ALTER TABLE ""VirusScanResult"" RENAME CONSTRAINT ""FK_VirusScanResult_File_Files_Id"" TO ""FK_VirusScanResult_Files_FileId"";
DROP TABLE ""EmailVerification"";
DROP TABLE ""PaymentOrderLightning"";
DROP TABLE ""PaymentOrder"";
DROP TABLE ""PaymentStrike"";
DROP TABLE ""Payment"";
DROP TABLE ""VersionInfo"";
", conn, tx).ExecuteNonQueryAsync();
// manually create init migration entry for EF to skip Init migration
await new NpgsqlCommand(@"
CREATE TABLE ""__EFMigrationsHistory"" (
""MigrationId"" varchar(150) NOT NULL,
""ProductVersion"" varchar(32) NOT NULL,
CONSTRAINT ""PK___EFMigrationsHistory"" PRIMARY KEY (""MigrationId"")
)", conn, tx).ExecuteNonQueryAsync();
await new NpgsqlCommand(
"INSERT INTO \"__EFMigrationsHistory\" (\"MigrationId\", \"ProductVersion\") VALUES('20230503115108_Init', '7.0.5')", conn,
tx)
.ExecuteNonQueryAsync();
await tx.CommitAsync();
}
}

View File

@ -1,52 +0,0 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
namespace VoidCat.Services.Migrations;
/// <inheritdoc />
public class FixSize : IMigration
{
private readonly ILogger<FixSize> _logger;
private readonly IFileMetadataStore _fileMetadata;
private readonly FileStoreFactory _fileStore;
public FixSize(ILogger<FixSize> logger, IFileMetadataStore fileMetadata, FileStoreFactory fileStore)
{
_logger = logger;
_fileMetadata = fileMetadata;
_fileStore = fileStore;
}
/// <inheritdoc />
public int Order => 2;
/// <inheritdoc />
public async ValueTask<IMigration.MigrationResult> Migrate(string[] args)
{
var files = await _fileMetadata.ListFiles<SecretFileMeta>(new(0, int.MaxValue));
await foreach (var file in files.Results)
{
try
{
var fs = await _fileStore.Open(new(file.Id, Enumerable.Empty<RangeRequest>()), CancellationToken.None);
if (file.Size != (ulong)fs.Length)
{
_logger.LogInformation("Updating file size {Id} to {Size}", file.Id, fs.Length);
var newFile = file with
{
Size = (ulong)fs.Length
};
await _fileMetadata.Set(newFile.Id, newFile);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fix file {id}", file.Id);
}
}
return IMigration.MigrationResult.Completed;
}
}

View File

@ -1,24 +0,0 @@
using FluentMigrator.Runner;
namespace VoidCat.Services.Migrations;
/// <inheritdoc />
public class FluentMigrationRunner : IMigration
{
private readonly IMigrationRunner _runner;
public FluentMigrationRunner(IMigrationRunner runner)
{
_runner = runner;
}
/// <inheritdoc />
public int Order => -1;
/// <inheritdoc />
public ValueTask<IMigration.MigrationResult> Migrate(string[] args)
{
_runner.MigrateUp();
return ValueTask.FromResult(IMigration.MigrationResult.Completed);
}
}

Some files were not shown because too many files have changed in this diff Show More