Compare commits

..

No commits in common. "main" and "v0.2" have entirely different histories.
main ... v0.2

341 changed files with 4767 additions and 29052 deletions

View File

@ -1,28 +0,0 @@
**/.classpath
**/.dockerignore
**/.env
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
**/appsettings.*.json
**/data
**/build
**/dist

View File

@ -1,21 +0,0 @@
---
kind: pipeline
type: kubernetes
name: default
metadata:
namespace: git
steps:
- name: build
image: docker
privileged: true
environment:
TOKEN:
from_secret: gitea
commands:
- dockerd &
- docker login -u kieran -p $TOKEN git.v0l.io
- docker buildx create --name mybuilder --bootstrap --use
- docker buildx build -t git.v0l.io/kieran/void-cat:latest --platform linux/amd64 --push .
- kill $(cat /var/run/docker.pid)

View File

@ -1,72 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ v4-re ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ v4-re ]
schedule:
- cron: '36 14 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'csharp', 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

15
.gitignore vendored
View File

@ -1,20 +1,7 @@
.vs/
bin/
obj/
data/
*.csproj.user
*.vcxproj.user
*.min.*
dist/
.vscode/
Debug/
Release/
node_modules/
package-lock.json
out/
sw.js
.DS_Store
.idea/
appsettings.*.json
VoidCat.xml
build/
.vscode/

View File

@ -1,32 +0,0 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app
#install npm
ENV NODE_MAJOR=20
RUN apt update && \
apt install -y ca-certificates curl gnupg && \
mkdir -p /etc/apt/keyrings && \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
apt update && \
apt install nodejs -y
#run yarn install
COPY . .
RUN git config --global --add safe.directory /app \
&& cd VoidCat/spa \
&& npx yarn \
&& npx yarn build
RUN rm -rf VoidCat/appsettings.*.json \
&& 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 \
&& apt clean \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "VoidCat.dll"]

110
README.md
View File

@ -1,88 +1,36 @@
## Void.cat
Free, simple file hosting
Setup
===
### Features
- Profiles
- File bandwidth statistics
- Administration features
- File download paywall
### Running
Use the docker image to run void.cat:
`docker run --rm -it -p 8080:80 git.v0l.io/kieran/void-cat:latest`
Then open your browser at http://localhost:8080.
**The first registration will be set as admin,
so make sure to create your own account**
### Deploying
Docker compose is the best option for most as this sets up postgres / redis / clamav.
Run the following commands to get going:
```bash
git clone https://git.v0l.io/Kieran/void.cat
cd void.cat/
docker compose up -d
* Nginx
* php-fpm
* php-redis
* php-curl
* php-gmp
* Redis
Nginx handler
====
```
You should now be able to access void.cat on `http://localhost`.
If you already have something running on port `80` you may have problems, you can modify the `docker-compose.yml`
file to change the port.
You can modify the site config in `./VoidCat/appsettings.compose.json`, this is recommended for anything other
than a simple test
### Usage
Simply drag and drop your files into the dropzone,
or paste your screenshots or files into the browser window.
From cli you can upload with `curl`:
```bash
export FILE=memes.jpg
curl -X POST \
-H "V-Content-Type: $(file --mime-type -b $FILE)" \
-H "V-Full-Digest: $(sha256sum -bz $FILE | cut -d' ' -f1)" \
-H "V-Filename: $FILE" \
--data-binary @$FILE \
"https://void.cat/upload?cli=true"
```
Or you can create an alias function in `~/bash_aliases` like so:
```bash
vcu() {
echo "Uploading $1"
curl -X POST \
-H "V-Content-Type: $(file --mime-type -b $1)" \
-H "V-Full-Digest: $(sha256sum -bz $1 | cut -d' ' -f1)" \
-H "V-Filename: $1" \
--data-binary @$1 \
"https://void.cat/upload?cli=true"
echo -e ""
location ~* "^\/([0-9a-z]{27})$" {
try_files $uri /src/php/handler.php?h=download&id=$1;
}
```
Uploading from cli will simply become `vcu memes.jpg`
Void Binary File Format (VBF)
===
| Name | Type | Description |
|---|---|---|
| version | uint8_t | Binary file format version |
| hash | SHA256 hash | The hash of the unencrypted file |
| uploaded | uint32_t | Timestamp of when the upload started |
| payload | >EOF | The encrypted payload |
You can also upload files to your user account by specifying an API key in the curl command:
```bash
-H "Authorization: Bearer MY_API_KEY"
```
---
VBF Payload Format
This command will return the direct download URL only.
To get the json output simply remove the `?cli=true` from the url.
### Development
To run postgres in local use:
```
docker run --rm -it -p 5432:5432 -e POSTGRES_DB=void -e POSTGRES_PASSWORD=postgres postgres -d postgres
```
To run MinIO in local use:
```
docker run --rm -it -p 9000:9000 -p 9001:9001 minio/minio -- server /data --console-address ":9001"
```
| Name | Type | Description |
|---|---|---|
| header_length | uint16_t | Length of the header section |
| header | string | The header json |
| file | >EOF | The file |

View File

@ -1,32 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32014.148
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoidCat", "VoidCat\VoidCat.csproj", "{4BF9EA98-D462-4665-8276-6D3B4CD50C3A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{40BC550E-CAD3-4E85-8BC9-539B53F87126}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
docker-compose.dev.yml = docker-compose.dev.yml
prometheus.yml = prometheus.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4BF9EA98-D462-4665-8276-6D3B4CD50C3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4BF9EA98-D462-4665-8276-6D3B4CD50C3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4BF9EA98-D462-4665-8276-6D3B4CD50C3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4BF9EA98-D462-4665-8276-6D3B4CD50C3A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B1192B04-DFA3-4A9D-A6A3-E4602EF06B96}
EndGlobalSection
EndGlobal

View File

@ -1,123 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
namespace VoidCat.Controllers.Admin;
[Route("admin")]
[Authorize(Policy = Policies.RequireAdmin)]
public class AdminController : Controller
{
private readonly FileStoreFactory _fileStore;
private readonly IFileMetadataStore _fileMetadata;
private readonly FileInfoManager _fileInfo;
private readonly IUserStore _userStore;
private readonly IUserUploadsStore _userUploads;
public AdminController(FileStoreFactory fileStore, IUserStore userStore, FileInfoManager fileInfo,
IFileMetadataStore fileMetadata, IUserUploadsStore userUploads)
{
_fileStore = fileStore;
_userStore = userStore;
_fileInfo = fileInfo;
_fileMetadata = fileMetadata;
_userUploads = userUploads;
}
/// <summary>
/// List all files in the system
/// </summary>
/// <param name="request">Page request</param>
/// <returns></returns>
[HttpOptions]
[HttpPost]
[Route("file")]
public async Task<RenderedResults<VoidFileResponse>> ListFiles([FromBody] PagedRequest request)
{
var files = await _fileMetadata.ListFiles(request);
return new()
{
Page = files.Page,
PageSize = files.PageSize,
TotalResults = files.TotalResults,
Results = (await files.Data.SelectAwait(a => _fileInfo.Get(a.Id, false)).ToListAsync())!
};
}
/// <summary>
/// Delete a file from the system
/// </summary>
/// <param name="id">Id of the file to delete</param>
[HttpOptions]
[HttpDelete]
[Route("file/{id}")]
public async Task DeleteFile([FromRoute] string id)
{
var gid = id.FromBase58Guid();
await _fileStore.DeleteFile(gid);
await _fileInfo.Delete(gid);
}
/// <summary>
/// List all users in the system
/// </summary>
/// <param name="request">Page request</param>
/// <returns></returns>
[HttpOptions]
[HttpPost]
[Route("users")]
public async Task<RenderedResults<AdminListedUser>> ListUsers([FromBody] PagedRequest request)
{
var result = await _userStore.ListUsers(request);
var ret = await result.Data.SelectAwait(async a =>
{
var uploads = await _userUploads.ListFiles(a.Id, new(0, int.MaxValue));
return new AdminListedUser(a.ToAdminApiUser(true), uploads.TotalResults);
}).ToListAsync();
return new()
{
PageSize = request.PageSize,
Page = request.Page,
TotalResults = result.TotalResults,
Results = ret
};
}
/// <summary>
/// Admin update user account
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
[HttpOptions]
[HttpPost]
[Route("update-user")]
public async Task<IActionResult> UpdateUser([FromBody] AdminUpdateUser user)
{
var oldUser = await _userStore.Get(user.Id);
if (oldUser == default) return BadRequest();
oldUser.Storage = user.Storage;
oldUser.Email = user.Email;
await _userStore.AdminUpdateUser(oldUser);
return Ok();
}
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

@ -1,216 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using VoidCat.Database;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Users;
namespace VoidCat.Controllers;
[Route("auth")]
public class AuthController : Controller
{
private readonly UserManager _manager;
private readonly VoidSettings _settings;
private readonly ICaptchaVerifier _captchaVerifier;
private readonly IApiKeyStore _apiKeyStore;
private readonly IUserStore _userStore;
public AuthController(UserManager userManager, VoidSettings settings, ICaptchaVerifier captchaVerifier,
IApiKeyStore apiKeyStore,
IUserStore userStore)
{
_manager = userManager;
_settings = settings;
_captchaVerifier = captchaVerifier;
_apiKeyStore = apiKeyStore;
_userStore = userStore;
}
/// <summary>
/// Login to a user account
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
[HttpPost]
[Route("login")]
public async Task<LoginResponse> Login([FromBody] LoginRequest req)
{
try
{
if (!TryValidateModel(req))
{
var error = ControllerContext.ModelState.FirstOrDefault().Value?.Errors.FirstOrDefault()?.ErrorMessage;
return new(null, error);
}
// check captcha
if (!await _captchaVerifier.Verify(req.Captcha))
{
return new(null, "Captcha verification failed");
}
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.ToApiUser(true));
}
catch (Exception ex)
{
return new(null, ex.Message);
}
}
/// <summary>
/// Register a new account
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
[HttpPost]
[Route("register")]
public async Task<LoginResponse> Register([FromBody] LoginRequest req)
{
try
{
if (!TryValidateModel(req))
{
var error = ControllerContext.ModelState.FirstOrDefault().Value?.Errors.FirstOrDefault()?.ErrorMessage;
return new(null, error);
}
// check captcha
if (!await _captchaVerifier.Verify(req.Captcha))
{
return new(null, "Captcha verification failed");
}
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.ToApiUser(true));
}
catch (Exception ex)
{
return new(null, ex.Message);
}
}
/// <summary>
/// Start OAuth2 authorize flow
/// </summary>
/// <param name="provider">OAuth provider</param>
/// <returns></returns>
[HttpGet]
[Route("{provider}")]
public IActionResult Authorize([FromRoute] string provider)
{
return Redirect(_manager.Authorize(provider).ToString());
}
/// <summary>
/// Authorize user from OAuth2 code grant
/// </summary>
/// <param name="code">Code used to generate access token</param>
/// <param name="provider">OAuth provider</param>
/// <returns></returns>
[HttpGet]
[Route("{provider}/token")]
public async Task<IActionResult> Token([FromRoute] string provider, [FromQuery] string code)
{
var newUser = await _manager.LoginOrRegister(code, provider);
var token = CreateToken(newUser, DateTime.UtcNow.AddHours(12));
var tokenWriter = new JwtSecurityTokenHandler();
return Redirect($"/login#{tokenWriter.WriteToken(token)}");
}
/// <summary>
/// List api keys for the user
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Route("api-key")]
public async Task<IActionResult> ListApiKeys()
{
var uid = HttpContext.GetUserId();
if (uid == default) return Unauthorized();
return Json(await _apiKeyStore.ListKeys(uid.Value));
}
/// <summary>
/// Create a new API key for the logged in user
/// </summary>
/// <param name="id"></param>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost]
[Route("api-key")]
public async Task<IActionResult> CreateApiKey([FromBody] CreateApiKeyRequest request)
{
var uid = HttpContext.GetUserId();
if (uid == default) return Unauthorized();
var user = await _userStore.Get(uid.Value);
if (user == default) return Unauthorized();
var expiry = DateTime.SpecifyKind(request.Expiry, DateTimeKind.Utc);
if (expiry > DateTime.UtcNow.AddYears(1))
{
return BadRequest();
}
var key = new ApiKey()
{
Id = Guid.NewGuid(),
UserId = user.Id,
Token = new JwtSecurityTokenHandler().WriteToken(CreateToken(user, expiry)),
Expiry = expiry
};
await _apiKeyStore.Add(key.Id, key);
return Json(key);
}
private JwtSecurityToken CreateToken(User user, DateTime expiry)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.JwtSettings.Key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>()
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(JwtRegisteredClaimNames.Exp, new DateTimeOffset(expiry).ToUnixTimeSeconds().ToString()),
new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
};
claims.AddRange(user.Roles.Select(a => new Claim(ClaimTypes.Role, a.Role)));
return new JwtSecurityToken(_settings.JwtSettings.Issuer, claims: claims,
signingCredentials: credentials);
}
public sealed class LoginRequest
{
public LoginRequest(string username, string password)
{
Username = username;
Password = password;
}
[Required] [EmailAddress] public string Username { get; }
[Required] [MinLength(6)] public string Password { get; }
public string? Captcha { get; init; }
}
public sealed record LoginResponse(string? Jwt, string? Error = null, ApiUser? Profile = null);
public sealed record CreateApiKeyRequest(DateTime Expiry);
}

View File

@ -1,223 +0,0 @@
using System.Net;
using Microsoft.AspNetCore.Mvc;
using VoidCat.Database;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
namespace VoidCat.Controllers;
public abstract class BaseDownloadController : Controller
{
private readonly ILogger _logger;
private readonly VoidSettings _settings;
private readonly FileInfoManager _fileInfo;
private readonly IPaymentOrderStore _paymentOrders;
private readonly IPaymentFactory _paymentFactory;
private readonly FileStoreFactory _storage;
protected BaseDownloadController(VoidSettings settings, FileInfoManager fileInfo, IPaymentOrderStore paymentOrders,
IPaymentFactory paymentFactory, ILogger logger, FileStoreFactory storage)
{
_settings = settings;
_fileInfo = fileInfo;
_paymentOrders = paymentOrders;
_paymentFactory = paymentFactory;
_logger = logger;
_storage = storage;
}
protected async Task SendResponse(string id, VoidFileResponse voidFile)
{
var gid = voidFile.Id;
if (id.EndsWith(".torrent"))
{
var t = await voidFile.Metadata.MakeTorrent(voidFile.Id,
await _storage.Open(new(gid, Enumerable.Empty<RangeRequest>()), CancellationToken.None),
_settings.SiteUrl, _settings.TorrentTrackers);
Response.Headers.ContentDisposition = $"inline; filename=\"{id}\"";
Response.ContentType = "application/x-bittorent";
await t.EncodeToAsync(Response.Body);
return;
}
var egressReq = new EgressRequest(gid, GetRanges(Request, (long)voidFile!.Metadata!.Size));
if (egressReq.Ranges.Count() > 1)
{
_logger.LogWarning("Multi-range request not supported!");
// downgrade to full send
egressReq = egressReq with
{
Ranges = Enumerable.Empty<RangeRequest>()
};
}
else if (egressReq.Ranges.Count() == 1)
{
Response.StatusCode = (int)HttpStatusCode.PartialContent;
if (egressReq.Ranges.Sum(a => a.Size) == 0)
{
Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
return;
}
}
else
{
Response.Headers.AcceptRanges = "bytes";
}
foreach (var range in egressReq.Ranges)
{
Response.Headers.Add("content-range", range.ToContentRange());
Response.ContentLength = range.Size;
}
var preResult = await _storage.StartEgress(egressReq);
if (preResult.Redirect != null)
{
Response.StatusCode = (int)HttpStatusCode.Redirect;
Response.Headers.Location = preResult.Redirect.ToString();
Response.ContentLength = 0;
return;
}
var cts = HttpContext.RequestAborted;
await Response.StartAsync(cts);
await _storage.Egress(egressReq, Response.Body, cts);
await Response.CompleteAsync();
}
protected async Task<VoidFileResponse?> SetupDownload(Guid id)
{
var meta = await _fileInfo.Get(id, false);
if (meta == null)
{
Response.StatusCode = 404;
return default;
}
return await CheckDownload(meta);
}
private async Task<VoidFileResponse?> CheckDownload(VoidFileResponse meta)
{
var origin = Request.Headers.Referer.FirstOrDefault() ?? Request.Headers.Origin.FirstOrDefault();
if (!string.IsNullOrEmpty(origin) && Uri.TryCreate(origin, UriKind.RelativeOrAbsolute, out var u))
{
if (_settings.BlockedOrigins.Any(a => string.Equals(a, u.DnsSafeHost, StringComparison.InvariantCultureIgnoreCase)))
{
Response.StatusCode = (int)HttpStatusCode.Forbidden;
return default;
}
}
// check payment order
if (meta.Payment != default && meta.Payment.Service != PaywallService.None && meta.Payment.Required)
{
var h402 = Request.Headers.FirstOrDefault(a => a.Key.Equals("Authorization", StringComparison.InvariantCultureIgnoreCase))
.Value.FirstOrDefault(a => a?.StartsWith("L402") ?? false);
var orderId = Request.Headers.GetHeader("V-OrderId") ?? h402 ?? Request.Query["orderId"];
if (!await IsOrderPaid(orderId!))
{
Response.Headers.CacheControl = "no-cache";
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
if (meta.Payment.Service is PaywallService.Strike or PaywallService.LnProxy)
{
var accept = Request.Headers.GetHeader("accept");
if (accept == "L402")
{
var provider = await _paymentFactory.CreateProvider(meta.Payment.Service);
var order = await provider.CreateOrder(meta.Payment!);
if (order != default)
{
Response.Headers.Add("access-control-expose-headers", "www-authenticate");
Response.Headers.Add("www-authenticate",
$"L402 macaroon=\"{Convert.ToBase64String(order.Id.ToByteArray())}\", invoice=\"{order!.OrderLightning!.Invoice}\"");
}
}
}
return default;
}
}
// prevent hot-linking viruses
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;
if (meta.VirusScan?.IsVirus == true && !hasCorrectReferer)
{
Response.StatusCode = (int)HttpStatusCode.Redirect;
Response.Headers.Location = $"/{meta.Id.ToBase58()}";
return default;
}
if (meta.IsNostr)
{
Response.StatusCode = (int)HttpStatusCode.Redirect;
Response.Headers.Location = $"https://nostr.download/{meta.Metadata.Digest}";
return default;
}
Response.Headers.XFrameOptions = "SAMEORIGIN";
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
Response.ContentLength = (long?)meta?.Metadata?.Size;
return meta;
}
private async ValueTask<bool> IsOrderPaid(string? orderId)
{
if (orderId?.StartsWith("L402") ?? false)
{
orderId = new Guid(Convert.FromBase64String(orderId.Substring(5).Split(":")[0])).ToString();
}
if (Guid.TryParse(orderId, out var oid))
{
var order = await _paymentOrders.Get(oid);
if (order?.Status == PaywallOrderStatus.Paid)
{
return true;
}
if (order?.Status is PaywallOrderStatus.Unpaid)
{
// check status
var svc = await _paymentFactory.CreateProvider(order.Service);
var status = await svc.GetOrderStatus(order.Id);
if (status != default && status.Status != order.Status)
{
await _paymentOrders.UpdateStatus(order.Id, status.Status);
}
if (status?.Status == PaywallOrderStatus.Paid)
{
return true;
}
}
}
return false;
}
private IEnumerable<RangeRequest> GetRanges(HttpRequest request, long totalSize)
{
foreach (var rangeHeader in request.Headers.Range)
{
if (string.IsNullOrEmpty(rangeHeader))
{
continue;
}
foreach (var h in RangeRequest.Parse(rangeHeader, totalSize))
{
yield return h;
}
}
}
}

View File

@ -1,44 +0,0 @@
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
namespace VoidCat.Controllers;
[Route("d")]
public class DownloadController : BaseDownloadController
{
public DownloadController(FileStoreFactory storage, ILogger<DownloadController> logger, FileInfoManager fileInfo,
IPaymentOrderStore paymentOrderStore, VoidSettings settings, IPaymentFactory paymentFactory)
: base(settings, fileInfo, paymentOrderStore, paymentFactory, logger, storage)
{
}
[HttpHead]
[HttpOptions]
[Route("{id}")]
[EnableCors("*")]
public Task DownloadFileOptions([FromRoute] string id)
{
var gid = id.FromBase58Guid();
return SetupDownload(gid);
}
/// <summary>
/// Download a specific file by Id
/// </summary>
/// <param name="id"></param>
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 86400)]
[HttpGet]
[Route("{id}")]
[EnableCors("*")]
public async Task DownloadFile([FromRoute] string id)
{
var gid = id.FromBase58Guid();
var voidFile = await SetupDownload(gid);
if (voidFile == default) return;
await SendResponse(id, voidFile);
}
}

View File

@ -1,139 +0,0 @@
using AngleSharp;
using AngleSharp.Dom;
using Microsoft.AspNetCore.Mvc;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Controllers;
public class IndexController : Controller
{
private readonly IWebHostEnvironment _webHost;
private readonly IFileMetadataStore _fileMetadata;
private readonly VoidSettings _settings;
public IndexController(IFileMetadataStore fileMetadata, IWebHostEnvironment webHost, VoidSettings settings)
{
_fileMetadata = fileMetadata;
_webHost = webHost;
_settings = settings;
}
/// <summary>
/// Return html content with tags for file preview
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[Route("{id}")]
[HttpGet]
public async Task<IActionResult> FilePreview(string id)
{
id.TryFromBase58Guid(out var gid);
var ubDownload = new UriBuilder(_settings.SiteUrl)
{
Path = $"/d/{gid.ToBase58()}"
};
var ubView = new UriBuilder(_settings.SiteUrl)
{
Path = $"/{gid.ToBase58()}"
};
var indexPath = Path.Combine(_webHost.WebRootPath, "index.html");
var indexContent = System.IO.File.Exists(indexPath) ?
await System.IO.File.ReadAllTextAsync(indexPath)
: string.Empty;
var meta = (await _fileMetadata.Get(gid))?.ToMeta(false);
var tags = new List<KeyValuePair<string, string>>()
{
new("site_name", "void.cat"),
new("title", meta?.Name ?? ""),
new("description", meta?.Description ?? ""),
new("url", ubView.Uri.ToString()),
};
var mime = meta?.MimeType;
if (mime?.StartsWith("image/") ?? false)
{
tags.Add(new("type", "image"));
tags.Add(new("image", ubDownload.Uri.ToString()));
tags.Add(new("image:type", mime));
}
else if (mime?.StartsWith("video/") ?? false)
{
tags.Add(new("type", "video.other"));
tags.Add(new("image", ""));
tags.Add(new("video", ubDownload.Uri.ToString()));
tags.Add(new("video:url", ubDownload.Uri.ToString()));
tags.Add(new("video:secure_url", ubDownload.Uri.ToString()));
tags.Add(new("video:type", mime));
}
else if (mime?.StartsWith("audio/") ?? false)
{
tags.Add(new("type", "audio.other"));
tags.Add(new("audio", ubDownload.Uri.ToString()));
tags.Add(new("audio:type", mime));
}
else
{
tags.Add(new("type", "website"));
}
var injectedHtml = await InjectTags(indexContent, tags);
return Content(injectedHtml?.ToHtml() ?? indexContent, "text/html");
}
public class IndexModel
{
public Guid Id { get; init; }
public VoidFileMeta? Meta { get; init; }
public AssetManifest Manifest { get; init; }
}
public class AssetManifest
{
public Dictionary<string, string> Files { get; init; }
public List<string> Entrypoints { get; init; }
}
private async Task<IDocument?> InjectTags(string html, List<KeyValuePair<string, string>> tags)
{
var config = Configuration.Default;
var context = BrowsingContext.New(config);
var doc = await context.OpenAsync(c => c.Content(html));
foreach (var tag in tags)
{
var ogTag = doc.CreateElement("meta");
ogTag.SetAttribute("property", $"og:{tag.Key}");
ogTag.SetAttribute("content", tag.Value);
doc.Head?.AppendChild(ogTag);
switch (tag.Key.ToLower())
{
case "title":
{
var titleTag = doc.Head?.QuerySelector("title");
if (titleTag != default)
{
titleTag.TextContent = tag.Value;
}
break;
}
case "description":
{
var descriptionTag = doc.Head?.QuerySelector("meta[name='description']");
descriptionTag?.SetAttribute("content", tag.Value);
break;
}
}
}
return doc;
}
}

View File

@ -1,65 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Controllers;
[Route("info")]
public class InfoController : Controller
{
private readonly IStatsReporter _statsReporter;
private readonly IFileMetadataStore _fileMetadata;
private readonly VoidSettings _settings;
private readonly ITimeSeriesStatsReporter _timeSeriesStats;
private readonly IEnumerable<string?> _fileStores;
private readonly IEnumerable<string> _oAuthProviders;
public InfoController(IStatsReporter statsReporter, IFileMetadataStore fileMetadata, VoidSettings settings,
ITimeSeriesStatsReporter stats, IEnumerable<IFileStore> fileStores, IEnumerable<IOAuthProvider> oAuthProviders)
{
_statsReporter = statsReporter;
_fileMetadata = fileMetadata;
_settings = settings;
_timeSeriesStats = stats;
_fileStores = fileStores.Select(a => a.Key);
_oAuthProviders = oAuthProviders.Select(a => a.Id);
}
/// <summary>
/// Return system info
/// </summary>
/// <returns></returns>
[HttpGet]
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
public async Task<GlobalInfo> GetGlobalStats()
{
var bw = await _statsReporter.GetBandwidth();
var storeStats = await _fileMetadata.Stats();
return new()
{
Bandwidth = bw,
TotalBytes = storeStats.Size,
Count = storeStats.Files,
BuildInfo = BuildInfo.GetBuildInfo(),
CaptchaSiteKey = _settings.CaptchaSettings?.SiteKey,
TimeSeriesMetrics = await _timeSeriesStats.GetBandwidth(DateTime.UtcNow.AddDays(-30), DateTime.UtcNow),
FileStores = _fileStores,
UploadSegmentSize = _settings.UploadSegmentSize,
OAuthProviders = _oAuthProviders
};
}
public sealed class GlobalInfo
{
public Bandwidth Bandwidth { get; init; }
public ulong TotalBytes { get; init; }
public long Count { get; init; }
public BuildInfo BuildInfo { get; init; }
public string? CaptchaSiteKey { get; init; }
public IEnumerable<BandwidthPoint> TimeSeriesMetrics { get; init; }
public IEnumerable<string?> FileStores { get; init; }
public ulong? UploadSegmentSize { get; init; }
public IEnumerable<string> OAuthProviders { get; init; }
}
}

View File

@ -1,242 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using VoidCat.Model;
using VoidCat.Services;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
using VoidCat.Services.Users;
using File = VoidCat.Database.File;
namespace VoidCat.Controllers;
[Route("nostr")]
public class NostrController : BaseDownloadController
{
private readonly VoidSettings _settings;
private readonly UserManager _userManager;
private readonly FileStoreFactory _storeFactory;
private readonly IFileMetadataStore _fileMetadata;
private readonly IUserUploadsStore _userUploads;
private readonly FileInfoManager _fileInfo;
public NostrController(VoidSettings settings, UserManager userManager, FileStoreFactory storeFactory, IFileMetadataStore fileMetadata,
IUserUploadsStore userUploads, FileInfoManager fileInfo, IPaymentOrderStore paymentOrderStore, IPaymentFactory paymentFactory,
ILogger<NostrController> logger)
: base(settings, fileInfo, paymentOrderStore, paymentFactory, logger, storeFactory)
{
_settings = settings;
_userManager = userManager;
_storeFactory = storeFactory;
_fileMetadata = fileMetadata;
_userUploads = userUploads;
_fileInfo = fileInfo;
}
[HttpGet("/.well-known/nostr/nip96.json")]
public IActionResult GetInfo()
{
var info = new Nip96Info
{
ApiUri = new Uri(_settings.SiteUrl, "/nostr"),
Plans = new()
{
{
"free", new Nip96Plan
{
Name = "Default",
MaxUploadSize = (long?)_settings.UploadSegmentSize
}
}
}
};
return Json(info);
}
[HttpPost]
[DisableRequestSizeLimit]
[DisableFormValueModelBinding]
[Authorize(AuthenticationSchemes = NostrAuth.Scheme, Policy = Policies.RequireNostr)]
public async Task<IActionResult> Upload()
{
var pubkey = HttpContext.GetPubKey();
if (string.IsNullOrEmpty(pubkey))
{
return Unauthorized();
}
try
{
if (_settings.MaintenanceMode)
{
throw new InvalidOperationException("Site is in maintenance mode");
}
var nostrUser = await _userManager.LoginOrRegister(pubkey);
var file = Request.Form.Files.First();
var meta = new File
{
MimeType = file.ContentType,
Name = file.FileName,
Description = Request.Form.TryGetValue("alt", out var sd) ? sd.First() : default,
Size = (ulong)file.Length,
Storage = nostrUser.Storage
};
var vf = await _storeFactory.Ingress(new(file.OpenReadStream(), meta, 1, 1, true), HttpContext.RequestAborted);
// save metadata
await _fileMetadata.Add(vf);
await _userUploads.AddFile(nostrUser.Id, vf.Id);
List<List<string>> tags = new()
{
new() {"url", new Uri(_settings.SiteUrl, $"/nostr/{vf.OriginalDigest}{Path.GetExtension(vf.Name)}").ToString()},
new() {"ox", vf.OriginalDigest ?? "", _settings.SiteUrl.ToString()},
new() {"x", vf.Digest ?? ""},
new() {"m", vf.MimeType}
};
if (!string.IsNullOrEmpty(vf.MediaDimensions))
{
tags.Add(new() {"dim", vf.MediaDimensions});
}
var ret = new Nip96UploadResult
{
FileHeader = new()
{
Tags = tags
}
};
return Json(ret);
}
catch (Exception ex)
{
return Json(new Nip96UploadResult()
{
Status = "error",
Message = ex.Message
});
}
}
[HttpGet("{id}")]
public async Task GetFile([FromRoute] string id)
{
var digest = Path.GetFileNameWithoutExtension(id);
var file = await _fileMetadata.GetHash(digest);
if (file == default)
{
Response.StatusCode = 404;
return;
}
var meta = await SetupDownload(file.Id);
if (meta == default) return;
await SendResponse(id, meta);
}
[HttpDelete("{id}")]
[Authorize(AuthenticationSchemes = NostrAuth.Scheme, Policy = Policies.RequireNostr)]
public async Task<IActionResult> DeleteFile([FromRoute] string id)
{
var digest = Path.GetFileNameWithoutExtension(id);
var file = await _fileMetadata.GetHash(digest);
if (file == default)
{
return NotFound();
}
var pubkey = HttpContext.GetPubKey();
if (string.IsNullOrEmpty(pubkey))
{
return Unauthorized();
}
var nostrUser = await _userManager.LoginOrRegister(pubkey);
var uploader = await _userUploads.Uploader(file.Id);
if (uploader == default || uploader != nostrUser.Id)
{
return Forbid();
}
await _fileInfo.Delete(file.Id);
return Json(new Nip96UploadResult());
}
}
public class Nip96Info
{
[JsonProperty("api_url")]
public Uri ApiUri { get; init; } = null!;
[JsonProperty("download_url")]
public Uri? DownloadUrl { get; init; }
[JsonProperty("delegated_to_url")]
public Uri? DelegatedTo { get; init; }
[JsonProperty("supported_nips")]
public List<int>? SupportedNips { get; init; }
[JsonProperty("tos_url")]
public Uri? Tos { get; init; }
[JsonProperty("content_types")]
public List<string>? ContentTypes { get; init; }
[JsonProperty("plans")]
public Dictionary<string, Nip96Plan>? Plans { get; init; }
}
public class Nip96Plan
{
[JsonProperty("name")]
public string Name { get; init; } = null!;
[JsonProperty("is_nip98_required")]
public bool Nip98Required { get; init; } = true;
[JsonProperty("url")]
public Uri? LandingPage { get; init; }
[JsonProperty("max_byte_size")]
public long? MaxUploadSize { get; init; }
[JsonProperty("file_expiration")]
public int[] FileExpiration { get; init; } = {0, 0};
[JsonProperty("media_transformations")]
public Nip96MediaTransformations? MediaTransformations { get; init; }
}
public class Nip96MediaTransformations
{
[JsonProperty("image")]
public List<string>? Image { get; init; }
}
public class Nip96UploadResult
{
[JsonProperty("status")]
public string Status { get; init; } = "success";
[JsonProperty("message")]
public string? Message { get; init; }
[JsonProperty("processing_url")]
public Uri? ProcessingUrl { get; init; }
[JsonProperty("nip94_event")]
public Nip94Info FileHeader { get; init; } = null!;
}
public class Nip94Info
{
[JsonProperty("tags")]
public List<List<string>> Tags { get; init; } = new();
}

View File

@ -1,420 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
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.Services.Abstractions;
using VoidCat.Services.Files;
using VoidCat.Services.Users;
using File = VoidCat.Database.File;
namespace VoidCat.Controllers
{
[Route("upload")]
public class UploadController : Controller
{
private readonly FileStoreFactory _storage;
private readonly IFileMetadataStore _metadata;
private readonly IPaymentStore _paymentStore;
private readonly IPaymentFactory _paymentFactory;
private readonly FileInfoManager _fileInfo;
private readonly IUserUploadsStore _userUploads;
private readonly IUserStore _userStore;
private readonly UserManager _userManager;
private readonly ITimeSeriesStatsReporter _timeSeriesStats;
private readonly VoidSettings _settings;
public UploadController(FileStoreFactory storage, IFileMetadataStore metadata, IPaymentStore payment,
IPaymentFactory paymentFactory, FileInfoManager fileInfo, IUserUploadsStore userUploads,
ITimeSeriesStatsReporter timeSeriesStats, IUserStore userStore, VoidSettings settings, UserManager userManager)
{
_storage = storage;
_metadata = metadata;
_paymentStore = payment;
_paymentFactory = paymentFactory;
_fileInfo = fileInfo;
_userUploads = userUploads;
_timeSeriesStats = timeSeriesStats;
_userStore = userStore;
_settings = settings;
_userManager = userManager;
}
/// <summary>
/// Primary upload endpoint
/// </summary>
/// <remarks>
/// Additional optional headers can be included to provide details about the file being uploaded:
///
/// `V-Content-Type` - Sets the `mimeType` of the file and is used on the preview page to display the file.
/// `V-Filename` - Sets the filename of the file.
/// `V-Description` - Sets the description of the file.
/// `V-Full-Digest` - Include a SHA256 hash of the entire file for verification purposes.
/// </remarks>
/// <param name="cli">True if you want to return only the url of the file in the response</param>
/// <returns>Returns <see cref="UploadResult"/></returns>
[HttpPost]
[HttpOptions]
[HttpHead]
[DisableRequestSizeLimit]
[DisableFormValueModelBinding]
[Authorize(AuthenticationSchemes = "Bearer,Nostr", Policy = Policies.RequireNostr)]
[AllowAnonymous]
public async Task<IActionResult> UploadFile([FromQuery] bool cli = false)
{
try
{
var mime = Request.Headers.GetHeader("V-Content-Type");
var stripMetadata = (mime?.StartsWith("image/") ?? false) || (Request.Headers.GetHeader("V-Strip-Metadata")
?.Equals("true", StringComparison.InvariantCultureIgnoreCase) ?? false);
if (_settings.MaintenanceMode)
{
throw new InvalidOperationException("Site is in maintenance mode");
}
var bodyLength = Request.ContentLength ?? 0;
if ((ulong)bodyLength >= (_settings.MaxFileSize ?? ulong.MaxValue))
{
throw new InvalidOperationException("File size too large");
}
var uid = HttpContext.GetUserId();
var pubkey = HttpContext.GetPubKey();
if (uid == default && !string.IsNullOrEmpty(pubkey))
{
var nostrUser = await _userManager.LoginOrRegister(pubkey);
uid = nostrUser.Id;
}
var filename = Request.Headers.GetHeader("V-Filename");
if (string.IsNullOrEmpty(mime) && !string.IsNullOrEmpty(filename))
{
if (new FileExtensionContentTypeProvider().TryGetContentType(filename, out var contentType))
{
mime = contentType;
}
}
if (string.IsNullOrEmpty(mime))
{
mime = "application/octet-stream";
}
// detect store for ingress
var store = _settings.DefaultFileStore;
if (uid.HasValue)
{
var user = await _userStore.Get(uid.Value);
if (user?.Storage != default)
{
store = user.Storage!;
}
}
var meta = new File
{
MimeType = mime,
Name = filename,
Description = Request.Headers.GetHeader("V-Description"),
Digest = Request.Headers.GetHeader("V-Full-Digest"),
Size = (ulong?)Request.ContentLength ?? 0UL,
Storage = store,
EncryptionParams = Request.Headers.GetHeader("V-EncryptionParams")
};
var (segment, totalSegments) = ParseSegmentsHeader();
var vf = await _storage.Ingress(new(Request.Body, meta, segment, totalSegments, stripMetadata),
HttpContext.RequestAborted);
// save metadata
await _metadata.Add(vf);
// attach file upload to user
if (uid.HasValue)
{
await _userUploads.AddFile(uid.Value, vf.Id);
}
if (cli)
{
var urlBuilder = new UriBuilder(_settings.SiteUrl)
{
Path = $"/d/{vf.Id.ToBase58()}"
};
return Content(urlBuilder.Uri.ToString(), "text/plain");
}
return Json(UploadResult.Success(vf.ToResponse(true)));
}
catch (Exception ex)
{
return Json(UploadResult.Error(ex.Message));
}
}
/// <summary>
/// Append data onto a file
/// </summary>
/// <remarks>
/// This endpoint is mainly used to bypass file upload limits enforced by CloudFlare.
/// Clients should split their uploads into segments, upload the first segment to the regular
/// upload endpoint, use the `editSecret` to append data to the file.
///
/// Set the edit secret in the header `V-EditSecret` otherwise you will not be able to append data.
/// </remarks>
/// <param name="id"></param>
/// <returns></returns>
[HttpPost]
[DisableRequestSizeLimit]
[DisableFormValueModelBinding]
[Route("{id}")]
public async Task<UploadResult> UploadFileAppend([FromRoute] string id)
{
try
{
var stripMetadata = Request.Headers.GetHeader("V-Strip-Metadata")
?.Equals("true", StringComparison.InvariantCultureIgnoreCase) ?? false;
if (_settings.MaintenanceMode && !stripMetadata)
{
throw new InvalidOperationException("Site is in maintenance mode");
}
var gid = id.FromBase58Guid();
var meta = await _metadata.Get(gid);
if (meta == default) return UploadResult.Error("File not found");
// Parse V-Segment header
var (segment, totalSegments) = ParseSegmentsHeader();
// sanity check for append operations
if (segment <= 1 || totalSegments <= 1)
{
return UploadResult.Error("Malformed request, segment must be > 1 for append");
}
var editSecret = Request.Headers.GetHeader("V-EditSecret");
var vf = await _storage.Ingress(new(Request.Body, meta, segment, totalSegments, stripMetadata)
{
EditSecret = editSecret?.FromBase58Guid() ?? Guid.Empty,
Id = gid
}, HttpContext.RequestAborted);
// update file size
await _metadata.Update(vf.Id, vf);
return UploadResult.Success(vf.ToResponse(true));
}
catch (Exception ex)
{
return UploadResult.Error(ex.Message);
}
}
/// <summary>
/// Return information about a specific file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Route("{id}")]
public async Task<IActionResult> GetInfo([FromRoute] string id)
{
if (!id.TryFromBase58Guid(out var fid)) return StatusCode(404);
var uid = HttpContext.GetUserId();
var isOwner = uid.HasValue && await _userUploads.Uploader(fid) == uid;
var info = await _fileInfo.Get(fid, isOwner || HttpContext.IsRole(Roles.Admin));
if (info == default) return StatusCode(404);
return Json(info);
}
/// <summary>
/// Return information about a specific file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Route("{id}/metrics")]
public async Task<IActionResult> Metrics([FromRoute] string id)
{
if (!id.TryFromBase58Guid(out var fid)) return StatusCode(404);
var stats = await _timeSeriesStats.GetBandwidth(fid, DateTime.UtcNow.Subtract(TimeSpan.FromDays(30)),
DateTime.UtcNow);
return Json(stats);
}
/// <summary>
/// Create a payment order to pay
/// </summary>
/// <param name="id">File id</param>
/// <returns></returns>
[HttpGet]
[Route("{id}/payment")]
public async ValueTask<PaywallOrder?> CreateOrder([FromRoute] string id)
{
var gid = id.FromBase58Guid();
var file = await _fileInfo.Get(gid, false);
var config = await _paymentStore.Get(gid);
var provider = await _paymentFactory.CreateProvider(config!.Service);
return await provider.CreateOrder(file!.Payment!);
}
/// <summary>
/// Return the status of an order
/// </summary>
/// <param name="id">File id</param>
/// <param name="order">Order id</param>
/// <returns></returns>
[HttpGet]
[Route("{id}/payment/{order:guid}")]
public async ValueTask<PaywallOrder?> GetOrderStatus([FromRoute] string id, [FromRoute] Guid order)
{
var gid = id.FromBase58Guid();
var config = await _paymentStore.Get(gid);
var provider = await _paymentFactory.CreateProvider(config!.Service);
return await provider.GetOrderStatus(order);
}
/// <summary>
/// Update the payment config
/// </summary>
/// <param name="id">File id</param>
/// <param name="req">Requested config to set on the file</param>
/// <returns></returns>
[HttpPost]
[Route("{id}/payment")]
public async Task<IActionResult> SetPaymentConfig([FromRoute] string id, [FromBody] SetPaymentConfigRequest req)
{
var gid = id.FromBase58Guid();
var meta = await _metadata.Get(gid);
if (meta == default) return NotFound();
if (!meta.CanEdit(req.EditSecret)) return Unauthorized();
if (req.StrikeHandle != default)
{
if (meta.Paywall?.Id != default)
{
await _paymentStore.Delete(meta.Paywall.Id);
}
await _paymentStore.Add(gid, new Paywall
{
FileId = meta.Id,
Service = PaywallService.Strike,
Upstream = req.StrikeHandle,
Amount = req.Amount,
Currency = Enum.Parse<PaywallCurrency>(req.Currency),
Required = req.Required
});
return Ok();
}
// if none set, delete existing config
if (meta.Paywall?.Id != default)
{
await _paymentStore.Delete(meta.Paywall.Id);
}
return Ok();
}
/// <summary>
/// Update metadata about file
/// </summary>
/// <param name="id">Id of file to edit</param>
/// <param name="fileMeta">New metadata to update</param>
/// <returns></returns>
/// <remarks>
/// You can only change `Name`, `Description` and `MimeType`
/// </remarks>
[HttpPost]
[Route("{id}/meta")]
public async Task<IActionResult> UpdateFileMeta([FromRoute] string id, [FromBody] VoidFileMeta fileMeta)
{
var gid = id.FromBase58Guid();
var meta = await _metadata.Get(gid);
if (meta == default) return NotFound();
if (!(meta.CanEdit(fileMeta.EditSecret) || HttpContext.IsRole(Roles.Admin))) return Unauthorized();
await _metadata.Update(gid, new()
{
Name = fileMeta.Name,
Description = fileMeta.Description,
Expires = fileMeta.Expires,
MimeType = fileMeta.MimeType
});
return Ok();
}
private (int Segment, int TotalSegments) ParseSegmentsHeader()
{
// Parse V-Segment header
int segment = 1, totalSegments = 1;
var segmentHeader = Request.Headers.GetHeader("V-Segment");
if (!string.IsNullOrEmpty(segmentHeader))
{
var split = segmentHeader.Split("/");
if (split.Length == 2 && int.TryParse(split[0], out var a) && int.TryParse(split[1], out var b))
{
segment = a;
totalSegments = b;
}
}
return (segment, totalSegments);
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
public record UploadResult(bool Ok, VoidFileResponse? File, string? ErrorMessage)
{
public static UploadResult Success(VoidFileResponse vf)
=> new(true, vf, null);
public static UploadResult Error(string message)
=> new(false, null, message);
}
public record SetPaymentConfigRequest
{
[JsonConverter(typeof(Base58GuidConverter))]
public Guid EditSecret { 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,177 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using VoidCat.Database;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
using UserFlags = VoidCat.Database.UserFlags;
namespace VoidCat.Controllers;
[Route("user/{id}")]
public class UserController : Controller
{
private readonly IUserStore _store;
private readonly IUserUploadsStore _userUploads;
private readonly IEmailVerification _emailVerification;
private readonly FileInfoManager _fileInfoManager;
public UserController(IUserStore store, IUserUploadsStore userUploads, IEmailVerification emailVerification,
FileInfoManager fileInfoManager)
{
_store = store;
_userUploads = userUploads;
_emailVerification = emailVerification;
_fileInfoManager = fileInfoManager;
}
/// <summary>
/// Return user profile
/// </summary>
/// <remarks>
/// You do not need to be logged in to return a user profile if their profile is set to public.
///
/// You may also use `me` as the `id` to get the logged in users profile.
/// </remarks>
/// <param name="id">User id to load</param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetUser([FromRoute] string id)
{
var loggedUser = HttpContext.GetUserId();
var isMe = id.Equals("me", StringComparison.InvariantCultureIgnoreCase);
if (isMe && !loggedUser.HasValue) return Unauthorized();
var requestedId = isMe ? loggedUser!.Value : id.FromBase58Guid();
var user = await _store.Get(requestedId);
if (user == default) return NotFound();
if (loggedUser != requestedId && !user.Flags.HasFlag(UserFlags.PublicProfile) && !HttpContext.IsRole(Roles.Admin))
return NotFound();
var isMyProfile = requestedId == user.Id;
return Json(user!.ToApiUser(isMyProfile));
}
/// <summary>
/// Update profile settings
/// </summary>
///
/// <param name="id">User id</param>
/// <param name="user"></param>
/// <returns></returns>
[HttpPost]
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();
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();
}
/// <summary>
/// Return a list of files which the user has uploaded
/// </summary>
/// <remarks>
/// This will return files if the profile has public uploads set on their profile.
/// Otherwise you can return your own uploaded files if you are logged in.
/// </remarks>
/// <param name="id">User id</param>
/// <param name="request">Page request</param>
/// <returns></returns>
[HttpPost]
[Route("files")]
public async Task<IActionResult> ListUserFiles([FromRoute] string id,
[FromBody] PagedRequest request)
{
var loggedUser = HttpContext.GetUserId();
var isAdmin = HttpContext.IsRole(Roles.Admin);
var user = await GetRequestedUser(id);
if (user == default) return NotFound();
// not logged in user files, check public flag
var canViewUploads = loggedUser == user.Id || isAdmin;
if (!canViewUploads &&
!user.Flags.HasFlag(UserFlags.PublicUploads)) return Forbid();
var results = await _userUploads.ListFiles(id.FromBase58Guid(), request);
var files = await results.Data.ToListAsync();
var fileInfo = await _fileInfoManager.Get(files.ToArray(), false);
return Json(new RenderedResults<VoidFileResponse>()
{
PageSize = results.PageSize,
Page = results.Page,
TotalResults = results.TotalResults,
Results = fileInfo.ToList()
});
}
/// <summary>
/// Send a verification code for a specific user
/// </summary>
/// <param name="id">User id to send code for</param>
/// <returns></returns>
[HttpGet]
[Route("verify")]
public async Task<IActionResult> SendVerificationCode([FromRoute] string id)
{
var user = await GetAuthorizedUser(id);
if (user == default) return Unauthorized();
var isEmailVerified = (user?.Flags.HasFlag(UserFlags.EmailVerified) ?? false);
if (isEmailVerified) return UnprocessableEntity();
await _emailVerification.SendNewCode(user!);
return Accepted();
}
/// <summary>
/// Confirm email verification code
/// </summary>
/// <param name="id">User id to verify</param>
/// <param name="req">Verification code to check</param>
/// <returns></returns>
[HttpPost]
[Route("verify")]
public async Task<IActionResult> VerifyCode([FromRoute] string id, [FromBody] VerifyCodeRequest req)
{
var user = await GetAuthorizedUser(id);
if (user == default) return Unauthorized();
if (!await _emailVerification.VerifyCode(user, req.Code)) return BadRequest();
user.Flags |= UserFlags.EmailVerified;
await _store.UpdateProfile(user);
return Accepted();
}
private async Task<User?> GetAuthorizedUser(string id)
{
var loggedUser = HttpContext.GetUserId();
var gid = id.FromBase58Guid();
var user = await _store.Get(gid);
return user?.Id != loggedUser ? default : user;
}
private async Task<User?> GetRequestedUser(string id)
{
var gid = id.FromBase58Guid();
return await _store.Get(gid);
}
public class VerifyCodeRequest
{
[JsonProperty("code")]
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Code { get; init; }
}
}

View File

@ -1,11 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,46 +0,0 @@
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.Property(a => a.OriginalDigest);
builder.Property(a => a.MediaDimensions);
builder.HasIndex(a => a.Uploaded);
builder.HasIndex(a => a.Digest);
builder.HasIndex(a => a.OriginalDigest);
}
}

View File

@ -1,30 +0,0 @@
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>(a => a.FileId);
builder.Property(a => a.Upstream);
}
}

View File

@ -1,30 +0,0 @@
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

@ -1,22 +0,0 @@
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

@ -1,36 +0,0 @@
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

@ -1,38 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,11 +0,0 @@
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; }
}

View File

@ -1,21 +0,0 @@
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 string? OriginalDigest { get; init; }
public string? MediaDimensions { get; init; }
public Paywall? Paywall { get; init; }
}

View File

@ -1,44 +0,0 @@
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 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 bool Required { get; init; } = true;
/// <summary>
/// Upstream identifier, handle or lnurl
/// </summary>
public string? Upstream { get; init; }
}

View File

@ -1,40 +0,0 @@
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; }
}

View File

@ -1,119 +0,0 @@
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,
/// <summary>
/// Nostr login
/// </summary>
Nostr = 4,
}

View File

@ -1,15 +0,0 @@
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

@ -1,10 +0,0 @@
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

@ -1,12 +0,0 @@
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

@ -1,353 +0,0 @@
// <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

@ -1,223 +0,0 @@
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

@ -1,497 +0,0 @@
// <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

@ -1,121 +0,0 @@
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

@ -1,501 +0,0 @@
// <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

@ -1,63 +0,0 @@
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

@ -1,474 +0,0 @@
// <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("20230529211008_SimplifyPaymentOrder")]
partial class SimplifyPaymentOrder
{
/// <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.Property<string>("Upstream")
.HasColumnType("text");
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.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.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.PaywallOrder", b =>
{
b.Navigation("OrderLightning");
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Navigation("Roles");
b.Navigation("UserFiles");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,50 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace VoidCat.Migrations
{
/// <inheritdoc />
public partial class SimplifyPaymentOrder : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PaymentStrike");
migrationBuilder.AddColumn<string>(
name: "Upstream",
table: "Payment",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Upstream",
table: "Payment");
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);
});
}
}
}

View File

@ -1,481 +0,0 @@
// <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("20230529211453_AddFileId")]
partial class AddFileId
{
/// <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")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<byte>("Currency")
.HasColumnType("smallint");
b.Property<Guid>("FileId")
.HasColumnType("uuid");
b.Property<bool>("Required")
.HasColumnType("boolean");
b.Property<int>("Service")
.HasColumnType("integer");
b.Property<string>("Upstream")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("FileId")
.IsUnique();
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.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", "FileId")
.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.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.PaywallOrder", b =>
{
b.Navigation("OrderLightning");
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Navigation("Roles");
b.Navigation("UserFiles");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,64 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace VoidCat.Migrations
{
/// <inheritdoc />
public partial class AddFileId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Payment_Files_Id",
table: "Payment");
migrationBuilder.AddColumn<Guid>(
name: "FileId",
table: "Payment",
type: "uuid",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.CreateIndex(
name: "IX_Payment_FileId",
table: "Payment",
column: "FileId",
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_Payment_Files_FileId",
table: "Payment",
column: "FileId",
principalTable: "Files",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Payment_Files_FileId",
table: "Payment");
migrationBuilder.DropIndex(
name: "IX_Payment_FileId",
table: "Payment");
migrationBuilder.DropColumn(
name: "FileId",
table: "Payment");
migrationBuilder.AddForeignKey(
name: "FK_Payment_Files_Id",
table: "Payment",
column: "Id",
principalTable: "Files",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -1,491 +0,0 @@
// <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("20231120132852_Nip96")]
partial class Nip96
{
/// <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>("MediaDimensions")
.HasColumnType("text");
b.Property<string>("MimeType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("application/octet-stream");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("OriginalDigest")
.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("Digest");
b.HasIndex("OriginalDigest");
b.HasIndex("Uploaded");
b.ToTable("Files", (string)null);
});
modelBuilder.Entity("VoidCat.Database.Paywall", 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<bool>("Required")
.HasColumnType("boolean");
b.Property<int>("Service")
.HasColumnType("integer");
b.Property<string>("Upstream")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("FileId")
.IsUnique();
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.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", "FileId")
.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.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.PaywallOrder", b =>
{
b.Navigation("OrderLightning");
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Navigation("Roles");
b.Navigation("UserFiles");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,56 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace VoidCat.Migrations
{
/// <inheritdoc />
public partial class Nip96 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "MediaDimensions",
table: "Files",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "OriginalDigest",
table: "Files",
type: "text",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Files_Digest",
table: "Files",
column: "Digest");
migrationBuilder.CreateIndex(
name: "IX_Files_OriginalDigest",
table: "Files",
column: "OriginalDigest");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Files_Digest",
table: "Files");
migrationBuilder.DropIndex(
name: "IX_Files_OriginalDigest",
table: "Files");
migrationBuilder.DropColumn(
name: "MediaDimensions",
table: "Files");
migrationBuilder.DropColumn(
name: "OriginalDigest",
table: "Files");
}
}
}

View File

@ -1,488 +0,0 @@
// <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>("MediaDimensions")
.HasColumnType("text");
b.Property<string>("MimeType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("application/octet-stream");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("OriginalDigest")
.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("Digest");
b.HasIndex("OriginalDigest");
b.HasIndex("Uploaded");
b.ToTable("Files", (string)null);
});
modelBuilder.Entity("VoidCat.Database.Paywall", 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<bool>("Required")
.HasColumnType("boolean");
b.Property<int>("Service")
.HasColumnType("integer");
b.Property<string>("Upstream")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("FileId")
.IsUnique();
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.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", "FileId")
.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.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.PaywallOrder", b =>
{
b.Navigation("OrderLightning");
});
modelBuilder.Entity("VoidCat.Database.User", b =>
{
b.Navigation("Roles");
b.Navigation("UserFiles");
});
#pragma warning restore 612, 618
}
}
}

View File

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

View File

@ -1,51 +0,0 @@
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,16 +0,0 @@
namespace VoidCat.Model;
/// <summary>
/// I/O bandwidth model
/// </summary>
/// <param name="Ingress"></param>
/// <param name="Egress"></param>
public sealed record Bandwidth(ulong Ingress, ulong Egress);
/// <summary>
/// I/O bandwidth model at a specific time
/// </summary>
/// <param name="Time"></param>
/// <param name="Ingress"></param>
/// <param name="Egress"></param>
public sealed record BandwidthPoint(DateTime Time, ulong Ingress, ulong Egress);

View File

@ -1,27 +0,0 @@
using System.Text.RegularExpressions;
using Newtonsoft.Json;
namespace VoidCat.Model;
public class Base58GuidConverter : JsonConverter<Guid>
{
public override void WriteJson(JsonWriter writer, Guid value, JsonSerializer serializer)
{
writer.WriteValue(value.ToBase58());
}
public override Guid ReadJson(JsonReader reader, Type objectType, Guid existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String && existingValue == Guid.Empty)
{
var str = reader.Value as string;
if ((str?.Contains('-') ?? false) && Guid.TryParse(str, out var g))
{
return g;
}
return str?.FromBase58Guid() ?? existingValue;
}
return existingValue;
}
}

View File

@ -1,31 +0,0 @@
using System.Reflection;
namespace VoidCat.Model;
public class BuildInfo
{
public string? Version { get; init; }
public string? GitHash { get; init; }
public DateTime BuildTime { get; init; }
public static BuildInfo GetBuildInfo()
{
var asm = Assembly.GetEntryAssembly();
var version = asm.GetName().Version;
var gitHash = asm
.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(attr => attr.Key == "GitHash")?.Value;
var buildTime = asm
.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(attr => attr.Key == "BuildTime");
return new()
{
Version = $"{version.Major}.{version.Minor}.{version.Build}",
GitHash = gitHash,
BuildTime = DateTime.FromBinary(long.Parse(buildTime?.Value ?? "0"))
};
}
}

View File

@ -1,5 +0,0 @@
namespace VoidCat.Model;
public sealed record EgressRequest(Guid Id, IEnumerable<RangeRequest> Ranges);
public sealed record EgressResult(Uri? Redirect = null);

View File

@ -1,13 +0,0 @@
namespace VoidCat.Model.Exceptions;
/// <summary>
/// Specified file was not found
/// </summary>
public class VoidFileNotFoundException : Exception
{
public VoidFileNotFoundException(Guid id)
{
Id = id;
}
public Guid Id { get; }
}

View File

@ -1,17 +0,0 @@
namespace VoidCat.Model.Exceptions;
/// <summary>
/// Specified id was not in the correct format
/// </summary>
public class VoidInvalidIdException : Exception
{
public VoidInvalidIdException(string id)
{
Id = id;
}
/// <summary>
/// The id in question
/// </summary>
public string Id { get; }
}

View File

@ -1,11 +0,0 @@
namespace VoidCat.Model.Exceptions;
/// <summary>
/// Operation is not allowed
/// </summary>
public class VoidNotAllowedException : Exception
{
public VoidNotAllowedException(string message) : base(message)
{
}
}

View File

@ -1,407 +0,0 @@
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Amazon;
using Amazon.Runtime;
using Amazon.S3;
using BencodeNET.Objects;
using BencodeNET.Torrents;
using VoidCat.Database;
using VoidCat.Model.Exceptions;
using File = VoidCat.Database.File;
namespace VoidCat.Model;
public static class Extensions
{
public static AmazonS3Client CreateClient(this S3BlobConfig c)
{
AWSConfigsS3.UseSignatureVersion4 = true;
return new AmazonS3Client(new BasicAWSCredentials(c.AccessKey, c.SecretKey),
new AmazonS3Config
{
RegionEndpoint = !string.IsNullOrEmpty(c.Region) ? RegionEndpoint.GetBySystemName(c.Region) : null,
ServiceURL = c.ServiceUrl?.ToString(),
UseHttp = c.ServiceUrl?.Scheme == "http",
ForcePathStyle = true
});
}
public static Guid? GetUserId(this HttpContext context)
{
var claimSub = context.User.Claims.FirstOrDefault(a => a.Type == ClaimTypes.NameIdentifier)?.Value;
return Guid.TryParse(claimSub, out var g) ? g : null;
}
public static string? GetPubKey(this HttpContext context)
{
var claim = context.User.Claims.FirstOrDefault(a => a.Type == ClaimTypes.Name);
return claim?.Value;
}
public static IEnumerable<string>? GetUserRoles(this HttpContext context)
{
return context?.User?.Claims?.Where(a => a.Type == ClaimTypes.Role)
?.Select(a => a?.Value!);
}
public static bool IsRole(this HttpContext context, string role)
{
return GetUserRoles(context)?.Contains(role) ?? false;
}
public static Guid FromBase58Guid(this string base58)
{
var enc = new NBitcoin.DataEncoders.Base58Encoder();
base58 = Path.GetFileNameWithoutExtension(base58);
var guidBytes = enc.DecodeData(base58);
if (guidBytes.Length != 16) throw new VoidInvalidIdException(base58);
return new Guid(guidBytes);
}
public static bool TryFromBase58Guid(this string base58, out Guid v)
{
try
{
v = base58.FromBase58Guid();
return true;
}
catch
{
// ignored
v = Guid.Empty;
return false;
}
}
public static string ToBase58(this Guid id)
{
var enc = new NBitcoin.DataEncoders.Base58Encoder();
return enc.EncodeData(id.ToByteArray());
}
public static string? GetHeader(this IHeaderDictionary headers, string key)
{
var h = headers
.FirstOrDefault(a => a.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase));
return !string.IsNullOrEmpty(h.Value.ToString()) ? h.Value.ToString() : default;
}
public static bool CanEdit(this File file, Guid? editSecret)
{
return file.EditSecret == editSecret;
}
public static string ToHex(this byte[] data)
{
return BitConverter.ToString(data).Replace("-", string.Empty).ToLower();
}
private static int HexToInt(char c)
{
switch (c)
{
case '0':
return 0;
case '1':
return 1;
case '2':
return 2;
case '3':
return 3;
case '4':
return 4;
case '5':
return 5;
case '6':
return 6;
case '7':
return 7;
case '8':
return 8;
case '9':
return 9;
case 'a':
case 'A':
return 10;
case 'b':
case 'B':
return 11;
case 'c':
case 'C':
return 12;
case 'd':
case 'D':
return 13;
case 'e':
case 'E':
return 14;
case 'f':
case 'F':
return 15;
default:
throw new FormatException("Unrecognized hex char " + c);
}
}
private static readonly byte[,] ByteLookup = new byte[,]
{
// low nibble
{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
},
// high nibble
{
0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0
}
};
public static byte[] FromHex(this string input)
{
var result = new byte[(input.Length + 1) >> 1];
var lastCell = result.Length - 1;
var lastChar = input.Length - 1;
for (var i = 0; i < input.Length; i++)
{
result[lastCell - (i >> 1)] |= ByteLookup[i & 1, HexToInt(input[lastChar - i])];
}
return result;
}
public static string HashPassword(this string password)
{
return password.Hash("pbkdf2");
}
public static string Hash(this string password, string algo, string? saltHex = null)
{
var bytes = Encoding.UTF8.GetBytes(password);
return Hash(bytes, algo, saltHex);
}
public static string Hash(this byte[] bytes, string algo, string? saltHex = null)
{
switch (algo)
{
case "md5":
{
var hash = MD5.Create().ComputeHash(bytes);
return $"md5:{hash.ToHex()}";
}
case "sha1":
{
var hash = SHA1.Create().ComputeHash(bytes);
return $"sha1:{hash.ToHex()}";
}
case "sha256":
{
var hash = SHA256.Create().ComputeHash(bytes);
return $"sha256:{hash.ToHex()}";
}
case "sha512":
{
var hash = SHA512.Create().ComputeHash(bytes);
return $"sha512:{hash.ToHex()}";
}
case "pbkdf2":
{
const int saltSize = 32;
const int iterations = 310_000;
var salt = new byte[saltSize];
if (saltHex == default)
{
RandomNumberGenerator.Fill(salt);
}
else
{
salt = saltHex.FromHex();
}
var pbkdf2 = new Rfc2898DeriveBytes(bytes, salt, iterations);
return $"pbkdf2:{salt.ToHex()}:{pbkdf2.GetBytes(salt.Length).ToHex()}";
}
}
throw new ArgumentException("Unknown algo", nameof(algo));
}
/// <summary>
/// Validate password matches hashed password
/// </summary>
/// <param name="vu"></param>
/// <param name="password"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public static bool CheckPassword(this Database.User vu, string password)
{
if (vu.AuthType != UserAuthType.Internal)
throw new InvalidOperationException("User type is not internal, cannot check password!");
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);
}
/// <summary>
/// Patch metadata
/// </summary>
/// <param name="oldMeta"></param>
/// <param name="meta"></param>
public static void Patch(this File oldMeta, File meta)
{
oldMeta.Description = meta.Description ?? oldMeta.Description;
oldMeta.Name = meta.Name ?? oldMeta.Name;
oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType;
oldMeta.Storage = meta.Storage ?? oldMeta.Storage;
oldMeta.Expires = meta.Expires;
oldMeta.EncryptionParams = meta.EncryptionParams ?? oldMeta.EncryptionParams;
}
public static bool HasPostgres(this VoidSettings settings)
=> !string.IsNullOrEmpty(settings.Postgres);
public static bool HasRedis(this VoidSettings settings)
=> !string.IsNullOrEmpty(settings.Redis);
public static bool HasPrometheus(this VoidSettings settings)
=> settings.Prometheus?.Url != null;
public static bool HasVirusScanner(this VoidSettings settings)
=> settings.VirusScanner?.ClamAV != default || settings.VirusScanner?.VirusTotal != default;
public static bool HasPlausible(this VoidSettings settings)
=> settings.PlausibleAnalytics?.Endpoint != null;
public static bool HasDiscord(this VoidSettings settings)
=> settings.Discord != null;
public static bool HasGoogle(this VoidSettings settings)
=> settings.Google != null;
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/{id.ToBase58()}"
};
async Task<byte[]> BuildPieces()
{
fileStream.Seek(0, SeekOrigin.Begin);
var chunks = (int)Math.Ceiling(meta.Size / (decimal)pieceSize);
var hashes = new byte[pieceHashLen * chunks];
var chunk = new byte[pieceSize];
for (var x = 0; x < chunks; x++)
{
var rLen = await fileStream.ReadAsync(chunk, 0, chunk.Length);
var hash = SHA1.HashData(chunk.AsSpan(0, rLen));
Buffer.BlockCopy(hash, 0, hashes, x * pieceHashLen, pieceHashLen);
}
return hashes;
}
// build magnet link
var t = new Torrent()
{
File = new()
{
FileName = meta.Name,
FileSize = (long)meta.Size
},
Comment = meta.Name,
CreationDate = meta.Uploaded,
IsPrivate = false,
PieceSize = pieceSize,
Pieces = await BuildPieces(),
// ReSharper disable once CoVariantArrayConversion
Trackers = trackers.Select(a => new[] {a}).ToArray(),
ExtraFields = new BDictionary
{
{"url-list", webSeed.ToString()}
}
};
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,
MediaDimensions = f.MediaDimensions
};
}
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,11 +0,0 @@
namespace VoidCat.Model;
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; }
public bool IsAppend => Segment > 1 && IsMultipart;
public bool IsMultipart => TotalSegments > 1;
}

View File

@ -1,48 +0,0 @@
namespace VoidCat.Model;
public abstract class PagedResult
{
public int Page { get; init; }
public int PageSize { get; init; }
public int Pages => TotalResults / PageSize;
public int TotalResults { get; init; }
public int Results { get; init; }
}
public sealed class PagedResult<T> : PagedResult
{
public IAsyncEnumerable<T> Data { get; init; } = null!;
public async Task<RenderedResults<T>> GetResults()
{
return new()
{
Page = Page,
PageSize = PageSize,
TotalResults = TotalResults,
Results = await Data.ToListAsync()
};
}
}
public sealed class RenderedResults<T> : PagedResult
{
public IList<T> Results { get; init; }
}
public sealed record PagedRequest(int Page, int PageSize, PagedSortBy SortBy = PagedSortBy.Name, PageSortOrder SortOrder = PageSortOrder.Asc);
public enum PagedSortBy : byte
{
Name,
Date,
Size,
Id
}
public enum PageSortOrder : byte
{
Asc,
Dsc
}

View File

@ -1,39 +0,0 @@
namespace VoidCat.Model;
public sealed record RangeRequest(long? TotalSize, long? Start, long? End)
{
private const long DefaultBufferSize = 1024L * 512L;
public string OriginalString { get; private init; }
public long? Size
=> (Start.HasValue ? (End ?? Math.Min(TotalSize!.Value - 1, Start.Value + DefaultBufferSize)) - Start.Value : End) + 1L;
/// <summary>
/// Return Content-Range header content for this range
/// </summary>
/// <returns></returns>
public string ToContentRange()
=> $"bytes {Start}-{End ?? (Start + Size - 1L)}/{TotalSize?.ToString() ?? "*"}";
public static IEnumerable<RangeRequest> Parse(string header, long totalSize)
{
var ranges = header.Replace("bytes=", string.Empty).Split(",");
foreach (var range in ranges)
{
var rangeValues = range.Split("-");
long? endByte = null, startByte = 0;
if (long.TryParse(rangeValues[1], out var endParsed))
endByte = endParsed;
if (long.TryParse(rangeValues[0], out var startParsed))
startByte = startParsed;
yield return new(totalSize, startByte, endByte)
{
OriginalString = range
};
}
}
}

View File

@ -1,13 +0,0 @@
namespace VoidCat.Model;
public static class Roles
{
public const string User = "User";
public const string Admin = "Admin";
}
public static class Policies
{
public const string RequireAdmin = "RequireAdmin";
public const string RequireNostr = "RequireNostr";
}

View File

@ -1,22 +0,0 @@
namespace VoidCat.Model;
/// <summary>
/// Results for virus scan of a single file
/// </summary>
public sealed class VirusStatus
{
/// <summary>
/// Time the file was scanned
/// </summary>
public DateTime ScanTime { get; init; }
/// <summary>
/// Detected virus names
/// </summary>
public string? Names { get; init; }
/// <summary>
/// If we consider this result as a virus or not
/// </summary>
public bool IsVirus { get; init; }
}

View File

@ -1,37 +0,0 @@
using Newtonsoft.Json;
using VoidCat.Database;
namespace VoidCat.Model;
/// <summary>
/// Primary response type for file information
/// </summary>
public class VoidFileResponse
{
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
public VoidFileMeta Metadata { get; init; } = null!;
public Paywall? Payment { get; init; }
public ApiUser? Uploader { get; set; }
public Bandwidth? Bandwidth { get; init; }
public VirusStatus? VirusScan { get; init; }
public bool IsNostr { get; init; }
}
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; }
public string? MediaDimensions { get; init; }
}

View File

@ -1,233 +0,0 @@
using VoidCat.Services.Strike;
namespace VoidCat.Model
{
/// <summary>
/// System settings
/// </summary>
public class VoidSettings
{
/// <summary>
/// Maintenance flag
/// </summary>
public bool MaintenanceMode { get; init; } = false;
/// <summary>
/// Base site url, used for redirect urls
/// </summary>
public Uri SiteUrl { get; init; }
/// <summary>
/// Data directory to store files in
/// </summary>
public string DataDirectory { get; init; } = "./data";
/// <summary>
/// Size in bytes to split uploads into chunks
/// </summary>
public ulong? UploadSegmentSize { get; init; }
/// <summary>
/// Max file size for upload
/// </summary>
public ulong? MaxFileSize { get; init; }
/// <summary>
/// Tor configuration
/// </summary>
public TorSettings? TorSettings { get; init; }
/// <summary>
/// JWT settings for login token signing
/// </summary>
public JwtSettings JwtSettings { get; init; } = new()
{
Issuer = "void_cat_internal",
Key = "default_key_void_cat_host"
};
/// <summary>
/// Redis database connection string
/// </summary>
public string? Redis { get; init; }
/// <summary>
/// Strike payment service api settings
/// </summary>
public StrikeApiSettings? Strike { get; init; }
/// <summary>
/// Email server settings
/// </summary>
public SmtpSettings? Smtp { get; init; }
/// <summary>
/// CORS origins
/// </summary>
public List<string> CorsOrigins { get; init; } = new();
/// <summary>
/// Cloud file storage settings
/// </summary>
public CloudStorageSettings? CloudStorage { get; init; }
/// <summary>
/// Virus scanner settings
/// </summary>
public VirusScannerSettings? VirusScanner { get; init; }
/// <summary>
/// Request header to unmask in the logs, otherwise all are masked
/// </summary>
public IEnumerable<string>? RequestHeadersLog { get; init; }
/// <summary>
/// hCaptcha settings
/// </summary>
public CaptchaSettings? CaptchaSettings { get; init; }
/// <summary>
/// Postgres database connection string
/// </summary>
public string? Postgres { get; init; }
/// <summary>
/// Prometheus server for querying metrics
/// </summary>
public PrometheusSettings? Prometheus { get; init; }
/// <summary>
/// Select where to store metadata, if not set "local-disk" will be used
/// </summary>
public string MetadataStore { get; init; } = "local-disk";
/// <summary>
/// Select which store to use for files storage, if not set "local-disk" will be used
/// </summary>
public string DefaultFileStore { get; init; } = "local-disk";
/// <summary>
/// Plausible Analytics endpoint url
/// </summary>
public PlausibleSettings? PlausibleAnalytics { get; init; }
/// <summary>
/// Discord application settings
/// </summary>
public OAuthDetails? Discord { get; init; }
/// <summary>
/// Google application settings
/// </summary>
public OAuthDetails? Google { get; init; }
/// <summary>
/// A list of trackers to attach to torrent files
/// </summary>
public List<string> TorrentTrackers { get; init; } = new()
{
"wss://tracker.btorrent.xyz",
"wss://tracker.openwebtorrent.com",
"udp://tracker.opentrackr.org:1337/announce",
"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; }
/// <summary>
/// Blocked origin hostnames
/// </summary>
public List<string> BlockedOrigins { get; init; } = new();
}
public sealed class TorSettings
{
public Uri TorControl { get; init; }
public string PrivateKey { get; init; }
public string ControlPassword { get; init; }
}
public sealed class JwtSettings
{
public string Issuer { get; init; }
public string Key { get; init; }
}
public sealed class SmtpSettings
{
public Uri? Server { get; init; }
public string? Username { get; init; }
public string? Password { get; init; }
}
public sealed class CloudStorageSettings
{
public S3BlobConfig[]? S3 { get; init; }
}
public sealed class S3BlobConfig
{
public string Name { get; init; } = null!;
public string? AccessKey { get; init; }
public string? SecretKey { get; init; }
public Uri? ServiceUrl { get; init; }
public string? Region { get; init; }
public string? BucketName { get; init; } = "void-cat";
public bool Direct { get; init; }
public bool SendChecksum { get; init; } = true;
public bool DisablePayloadSigning { get; init; }
}
public sealed class VirusScannerSettings
{
public ClamAVSettings? ClamAV { get; init; }
public VirusTotalConfig? VirusTotal { get; init; }
}
public sealed class ClamAVSettings
{
public Uri? Endpoint { get; init; }
public long? MaxStreamSize { get; init; }
}
public sealed class VirusTotalConfig
{
public string? ApiKey { get; init; }
}
public sealed class CaptchaSettings
{
public string? SiteKey { get; init; }
public string? Secret { get; init; }
}
public sealed class PrometheusSettings
{
public Uri? Url { get; init; }
public string? EgressQuery { get; init; }
}
public sealed class PlausibleSettings
{
public Uri? Endpoint { get; init; }
public string? Domain { get; init; }
}
public sealed class OAuthDetails
{
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,38 +0,0 @@
@using VoidCat.Model
@using VoidCat.Services.Users
@model VoidCat.Database.EmailVerification
<!DOCTYPE html>
<html lang="en">
<head>
<title>void.cat - Email Verification Code</title>
<style>
body {
background-color: black;
color: white;
font-family: 'Source Code Pro', monospace;
}
.page {
width: 720px;
margin-left: auto;
margin-right: auto;
}
pre {
padding: 10px;
font-size: 24px;
background-color: #eee;
width: fit-content;
color: black;
user-select: all;
}
</style>
</head>
<body>
<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>
<p>This code will expire in @BaseEmailVerification.HoursExpire hours</p>
</div>
</body>
</html>

View File

@ -1,184 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Prometheus;
using VoidCat.Model;
using VoidCat.Services;
using VoidCat.Services.Analytics;
using VoidCat.Services.Migrations;
namespace VoidCat;
static class Program
{
[Flags]
enum RunModes
{
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"))
{
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))
{
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.Webserver))
{
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
var voidSettings = configuration.GetSection("Settings").Get<VoidSettings>();
services.AddSingleton(voidSettings);
services.AddSingleton(voidSettings.Strike ?? new());
var seqSettings = configuration.GetSection("Seq");
builder.Logging.AddSeq(seqSettings);
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);
}
app.UseStaticFiles();
app.UseHttpLogging();
app.UseRouting();
app.UseHttpMetrics();
app.UseCors();
app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();
app.UseHealthChecks("/healthz");
app.UseMiddleware<AnalyticsMiddleware>();
app.UseEndpoints(ep =>
{
ep.MapControllers();
ep.MapMetrics();
ep.MapRazorPages();
ep.MapFallbackToFile("index.html");
});
await app.RunAsync();
}
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());
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();
}
}
}
private static void ConfigureDb(IServiceCollection services, VoidSettings settings)
{
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;
}
}

View File

@ -1,21 +0,0 @@
{
"profiles": {
"VoidCat": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:7195",
"launchUrl": "http://localhost:3000",
"dotnetRunMessages": true
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true,
"useSSL": true
}
}
}

View File

@ -1,5 +0,0 @@
namespace VoidCat.Services.Abstractions;
public interface IAggregateStatsCollector : IStatsCollector
{
}

View File

@ -1,16 +0,0 @@
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Api key store
/// </summary>
public interface IApiKeyStore : IBasicStore<ApiKey>
{
/// <summary>
/// Return a list of Api keys for a given user
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<IReadOnlyList<ApiKey>> ListKeys(Guid id);
}

View File

@ -1,30 +0,0 @@
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Simple CRUD interface for data stores
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IBasicStore<T>
{
/// <summary>
/// Get a single item from the store
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<T?> Get(Guid id);
/// <summary>
/// Add an item to the store
/// </summary>
/// <param name="id"></param>
/// <param name="obj"></param>
/// <returns></returns>
ValueTask Add(Guid id, T obj);
/// <summary>
/// Delete an item from the store
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask Delete(Guid id);
}

View File

@ -1,56 +0,0 @@
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Basic KV cache interface
/// </summary>
public interface ICache
{
/// <summary>
/// Get a single object from cache by its key
/// </summary>
/// <param name="key"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
ValueTask<T?> Get<T>(string key);
/// <summary>
/// Set the the value of a key in the cache
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="expire"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
ValueTask Set<T>(string key, T value, TimeSpan? expire = null);
/// <summary>
/// Delete an object from the cache
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
ValueTask Delete(string key);
/// <summary>
/// Return a list of items at the specified key
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
ValueTask<string[]> GetList(string key);
/// <summary>
/// Add an item to the list at the specified key
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <returns></returns>
ValueTask AddToList(string key, string value);
/// <summary>
/// Remove an item from the list at a the specified key
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <returns></returns>
ValueTask RemoveFromList(string key, string value);
}

View File

@ -1,11 +0,0 @@
namespace VoidCat.Services.Abstractions;
public interface ICaptchaVerifier
{
/// <summary>
/// Verify captcha token is valid
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
ValueTask<bool> Verify(string? token);
}

View File

@ -1,21 +0,0 @@
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
public interface IEmailVerification
{
/// <summary>
/// Send email verification code
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
ValueTask<EmailVerification> SendNewCode(User user);
/// <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,73 +0,0 @@
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
{
/// <summary>
/// Get metadata for a single file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<File?> Get(Guid id);
/// <summary>
/// Get metadata for a single file by its hash
/// </summary>
/// <param name="digest"></param>
/// <returns></returns>
ValueTask<File?> GetHash(string digest);
/// <summary>
/// Get metadata for multiple files
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
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>
/// <returns></returns>
ValueTask Update(Guid id, File meta);
/// <summary>
/// List all files in the store
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
ValueTask<PagedResult<File>> ListFiles(PagedRequest request);
/// <summary>
/// Returns basic stats about the file store
/// </summary>
/// <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>
/// <param name="Files"></param>
/// <param name="Size"></param>
public sealed record StoreStats(long Files, ulong Size);
}

View File

@ -1,61 +0,0 @@
using VoidCat.Model;
using File = VoidCat.Database.File;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// File binary data store
/// </summary>
public interface IFileStore
{
/// <summary>
/// Return key for named instance
/// </summary>
string? Key { get; }
/// <summary>
/// Check if a file exists in the store
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<bool> Exists(Guid id);
/// <summary>
/// Ingress a file into the system (Upload)
/// </summary>
/// <param name="payload"></param>
/// <param name="cts"></param>
/// <returns></returns>
ValueTask<File> Ingress(IngressPayload payload, CancellationToken cts);
/// <summary>
/// Egress a file from the system (Download)
/// </summary>
/// <param name="request"></param>
/// <param name="outStream"></param>
/// <param name="cts"></param>
/// <returns></returns>
ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts);
/// <summary>
/// Pre-Egress checks
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
ValueTask<EgressResult> StartEgress(EgressRequest request);
/// <summary>
/// Deletes file data only, metadata must be deleted with <see cref="IFileInfoManager.Delete"/>
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask DeleteFile(Guid id);
/// <summary>
/// Open a filestream for a file on the system
/// </summary>
/// <param name="request"></param>
/// <param name="cts"></param>
/// <returns></returns>
ValueTask<Stream> Open(EgressRequest request, CancellationToken cts);
}

View File

@ -1,34 +0,0 @@
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// OAuth2 code grant provider
/// </summary>
public interface IOAuthProvider
{
/// <summary>
/// Id of this provider
/// </summary>
string Id { get; }
/// <summary>
/// Generate authorization code grant uri
/// </summary>
/// <returns></returns>
Uri Authorize();
/// <summary>
/// Get access token from auth code
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
ValueTask<UserAuthToken> GetToken(string code);
/// <summary>
/// Get a user object which represents this external account authorization
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
ValueTask<User?> GetUserDetails(UserAuthToken token);
}

View File

@ -1,16 +0,0 @@
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Factory class to access service provider implementations
/// </summary>
public interface IPaymentFactory
{
/// <summary>
/// Create provider handler for specified service type
/// </summary>
/// <param name="svc"></param>
/// <returns></returns>
ValueTask<IPaymentProvider> CreateProvider(PaywallService svc);
}

View File

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

View File

@ -1,23 +0,0 @@
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Provider to generate orders for a specific config
/// </summary>
public interface IPaymentProvider
{
/// <summary>
/// Create an order with the provider
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
ValueTask<PaywallOrder?> CreateOrder(Paywall file);
/// <summary>
/// Get the status of an existing order with the provider
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<PaywallOrder?> GetOrderStatus(Guid id);
}

View File

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

View File

@ -1,7 +0,0 @@
namespace VoidCat.Services.Abstractions;
public interface IStatsCollector
{
ValueTask TrackIngress(Guid id, ulong amount);
ValueTask TrackEgress(Guid id, ulong amount);
}

View File

@ -1,29 +0,0 @@
using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Get metrics from the system
/// </summary>
public interface IStatsReporter
{
/// <summary>
/// Get global total bandwidth
/// </summary>
/// <returns></returns>
ValueTask<Bandwidth> GetBandwidth();
/// <summary>
/// Get global bandwidth for a single file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<Bandwidth> GetBandwidth(Guid id);
/// <summary>
/// Delete bandwidth data for a single file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask Delete(Guid id);
}

View File

@ -1,9 +0,0 @@
using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
public interface ITimeSeriesStatsReporter
{
ValueTask<IReadOnlyList<BandwidthPoint>> GetBandwidth(DateTime start, DateTime end);
ValueTask<IReadOnlyList<BandwidthPoint>> GetBandwidth(Guid id, DateTime start, DateTime end);
}

View File

@ -1,10 +0,0 @@
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// User access token store
/// </summary>
public interface IUserAuthTokenStore : IBasicStore<UserAuthToken>
{
}

View File

@ -1,67 +0,0 @@
using VoidCat.Database;
using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// User store
/// </summary>
public interface IUserStore
{
/// <summary>
/// Get a single user
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
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
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
ValueTask<Guid?> LookupUser(string email);
/// <summary>
/// List all users in the system
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
ValueTask<PagedResult<User>> ListUsers(PagedRequest request);
/// <summary>
/// Update a users profile
/// </summary>
/// <param name="newUser"></param>
/// <returns></returns>
ValueTask UpdateProfile(User newUser);
/// <summary>
/// Updates the last login timestamp for the user
/// </summary>
/// <param name="id"></param>
/// <param name="timestamp"></param>
/// <returns></returns>
ValueTask UpdateLastLogin(Guid id, DateTime timestamp);
/// <summary>
/// Update user account for admin
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
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,32 +0,0 @@
using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Mapping store to associate files to users
/// </summary>
public interface IUserUploadsStore
{
/// <summary>
/// List all files for the user
/// </summary>
/// <param name="user"></param>
/// <param name="request"></param>
/// <returns></returns>
ValueTask<PagedResult<Guid>> ListFiles(Guid user, PagedRequest request);
/// <summary>
/// Assign a file upload to a user
/// </summary>
/// <param name="user"></param>
/// <param name="file"></param>
/// <returns></returns>
ValueTask AddFile(Guid user, Guid file);
/// <summary>
/// Get the uploader of a single file
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
ValueTask<Guid?> Uploader(Guid file);
}

View File

@ -1,16 +0,0 @@
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Store for virus scan results
/// </summary>
public interface IVirusScanStore : IBasicStore<VirusScanResult>
{
/// <summary>
/// Get the latest scan result by file id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<VirusScanResult?> GetByFile(Guid id);
}

View File

@ -1,17 +0,0 @@
using VoidCat.Database;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// File virus scanning interface
/// </summary>
public interface IVirusScanner
{
/// <summary>
/// Scan a single file
/// </summary>
/// <param name="id"></param>
/// <param name="cts"></param>
/// <returns></returns>
ValueTask<VirusScanResult> ScanFile(Guid id, CancellationToken cts);
}

View File

@ -1,6 +0,0 @@
namespace VoidCat.Services.Abstractions;
public interface IWebAnalyticsCollector
{
Task TrackPageView(HttpContext context);
}

View File

@ -1,32 +0,0 @@
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Analytics;
public class AnalyticsMiddleware : IMiddleware
{
private readonly ILogger<AnalyticsMiddleware> _logger;
private readonly IEnumerable<IWebAnalyticsCollector> _collectors;
public AnalyticsMiddleware(IEnumerable<IWebAnalyticsCollector> collectors, ILogger<AnalyticsMiddleware> logger)
{
_collectors = collectors;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
foreach (var collector in _collectors)
{
try
{
await collector.TrackPageView(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to track page view");
}
}
await next(context);
}
}

View File

@ -1,21 +0,0 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Analytics;
public static class AnalyticsStartup
{
/// <summary>
/// Add services needed to collect analytics
/// </summary>
/// <param name="services"></param>
/// <param name="settings"></param>
public static void AddAnalytics(this IServiceCollection services, VoidSettings settings)
{
services.AddTransient<AnalyticsMiddleware>();
if (settings.HasPlausible())
{
services.AddTransient<IWebAnalyticsCollector, PlausibleAnalytics>();
}
}
}

View File

@ -1,92 +0,0 @@
using System.Text;
using Newtonsoft.Json;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Analytics;
public class PlausibleAnalytics : IWebAnalyticsCollector
{
private readonly HttpClient _client;
private readonly ILogger<PlausibleAnalytics> _logger;
private readonly string _domain;
private readonly Uri _siteUrl;
public PlausibleAnalytics(HttpClient client, VoidSettings settings, ILogger<PlausibleAnalytics> logger)
{
_client = client;
_logger = logger;
_client.BaseAddress = settings.PlausibleAnalytics!.Endpoint!;
_client.Timeout = TimeSpan.FromSeconds(1);
_domain = settings.PlausibleAnalytics!.Domain!;
_siteUrl = settings.SiteUrl;
}
public async Task TrackPageView(HttpContext context)
{
var request = new HttpRequestMessage(HttpMethod.Post, "/api/event");
request.Headers.UserAgent.ParseAdd(context.Request.Headers.UserAgent);
if (context.Request.Headers.TryGetValue("x-forwarded-for", out var xff))
{
foreach (var xf in xff)
{
request.Headers.Add("x-forwarded-for", xf);
}
}
var ub = new UriBuilder(_siteUrl)
{
Path = context.Request.Path,
Query = context.Request.QueryString.ToUriComponent()
};
var ev = new EventObj(_domain, ub.Uri)
{
Referrer =
context.Request.Headers.Referer.Any()
? new Uri(context.Request.Headers.Referer.ToString())
: null
};
var json = JsonConvert.SerializeObject(ev, new JsonSerializerSettings()
{
NullValueHandling = NullValueHandling.Ignore
});
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
_logger.LogDebug("Sending pageview {request} {json}", request.ToString(), json);
var rsp = await _client.SendAsync(request);
if (!rsp.IsSuccessStatusCode)
{
throw new Exception(
$"Invalid plausible analytics response {rsp.StatusCode} {await rsp.Content.ReadAsStringAsync()}");
}
}
internal class EventObj
{
public EventObj(string domain, Uri url)
{
Domain = domain;
Url = url;
}
[JsonProperty("name")]
public string Name { get; init; } = "pageview";
[JsonProperty("domain")]
public string Domain { get; init; }
[JsonProperty("url")]
public Uri Url { get; init; }
[JsonProperty("screen_width")]
public int? ScreenWidth { get; init; }
[JsonProperty("referrer")]
public Uri? Referrer { get; init; }
[JsonProperty("props")]
public object? Props { get; init; }
}
}

View File

@ -1,60 +0,0 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
namespace VoidCat.Services.Background;
/// <summary>
/// Delete expired files
/// </summary>
public sealed class DeleteExpiredFiles : BackgroundService
{
private readonly ILogger<DeleteExpiredFiles> _logger;
private readonly IServiceScopeFactory _scopeFactory;
public DeleteExpiredFiles(ILogger<DeleteExpiredFiles> logger, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _scopeFactory.CreateScope();
var metadata = scope.ServiceProvider.GetRequiredService<IFileMetadataStore>();
var fileInfoManager = scope.ServiceProvider.GetRequiredService<FileInfoManager>();
var fileStoreFactory = scope.ServiceProvider.GetRequiredService<FileStoreFactory>();
var files = await metadata.ListFiles(new(0, int.MaxValue));
await foreach (var f in files.Data.WithCancellation(stoppingToken))
{
try
{
if (f.Expires < DateTime.Now)
{
await fileStoreFactory.DeleteFile(f.Id);
await fileInfoManager.Delete(f.Id);
_logger.LogInformation("Deleted file: {Id}", f.Id);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete file: {Id}", f.Id);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to run delete expired file services");
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}

View File

@ -1,61 +0,0 @@
using VoidCat.Database;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
namespace VoidCat.Services.Background;
public class DeleteUnverifiedAccounts : BackgroundService
{
private readonly ILogger<DeleteUnverifiedAccounts> _logger;
private readonly IServiceScopeFactory _scopeFactory;
public DeleteUnverifiedAccounts(ILogger<DeleteUnverifiedAccounts> logger, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _scopeFactory.CreateScope();
var userStore = scope.ServiceProvider.GetRequiredService<IUserStore>();
var userUploads = scope.ServiceProvider.GetRequiredService<IUserUploadsStore>();
var fileStore = scope.ServiceProvider.GetRequiredService<FileStoreFactory>();
var fileInfoManager = scope.ServiceProvider.GetRequiredService<FileInfoManager>();
var accounts = await userStore.ListUsers(new(0, Int32.MaxValue));
await foreach (var account in accounts.Data.WithCancellation(stoppingToken))
{
if (!account.Flags.HasFlag(UserFlags.EmailVerified) &&
account.Created.AddDays(7) < DateTimeOffset.UtcNow)
{
_logger.LogInformation("Deleting un-verified account: {Id}", account.Id.ToBase58());
await userStore.Delete(account.Id);
var files = await userUploads.ListFiles(account.Id, new(0, Int32.MinValue));
// ReSharper disable once UseCancellationTokenForIAsyncEnumerable
await foreach (var file in files.Data)
{
await fileStore.DeleteFile(file);
await fileInfoManager.Delete(file);
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete unverified accounts");
}
finally
{
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
}

View File

@ -1,67 +0,0 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.VirusScanner.Exceptions;
namespace VoidCat.Services.Background;
public class VirusScannerService : BackgroundService
{
private readonly ILogger<VirusScannerService> _logger;
private readonly IVirusScanner _scanner;
private readonly IFileMetadataStore _fileStore;
private readonly IVirusScanStore _scanStore;
public VirusScannerService(ILogger<VirusScannerService> logger, IVirusScanner scanner, IVirusScanStore scanStore,
IFileMetadataStore fileStore)
{
_scanner = scanner;
_logger = logger;
_scanStore = scanStore;
_fileStore = fileStore;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Virus scanner background service starting..");
while (!stoppingToken.IsCancellationRequested)
{
var page = 0;
while (true)
{
var files = await _fileStore.ListFiles(new(page++, 1_000));
if (files.Results == 0) break;
await foreach (var file in files.Data.WithCancellation(stoppingToken))
{
// file is too large, cant scan
if (file.Size > 4_000_000) continue;
// check for scans
var scan = await _scanStore.GetByFile(file.Id);
if (scan == default || scan.ScanTime < DateTime.UtcNow.Subtract(TimeSpan.FromDays(30)))
{
try
{
var result = await _scanner.ScanFile(file.Id, stoppingToken);
await _scanStore.Add(result.Id, result);
_logger.LogInformation("Scanned file {Id}, IsVirus = {Result}", result.File, result.Score);
}
catch (RateLimitedException rx)
{
var sleep = rx.RetryAfter ?? DateTimeOffset.UtcNow.AddMinutes(10);
_logger.LogWarning("VirusScanner was rate limited, sleeping until {Time}", sleep);
await Task.Delay(sleep - DateTimeOffset.UtcNow, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to scan file {Id} error={Message}", file.Id, ex.Message);
}
}
}
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}

View File

@ -1,39 +0,0 @@
using VoidCat.Services.Abstractions;
namespace VoidCat.Services;
/// <inheritdoc />
public abstract class BasicCacheStore<TStore> : IBasicStore<TStore>
{
protected readonly ICache Cache;
protected BasicCacheStore(ICache cache)
{
Cache = cache;
}
/// <inheritdoc />
public virtual ValueTask<TStore?> Get(Guid id)
{
return Cache.Get<TStore>(MapKey(id));
}
/// <inheritdoc />
public virtual ValueTask Add(Guid id, TStore obj)
{
return Cache.Set(MapKey(id), obj);
}
/// <inheritdoc />
public virtual ValueTask Delete(Guid id)
{
return Cache.Delete(MapKey(id));
}
/// <summary>
/// Map an id to a key in the KV store
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
protected abstract string MapKey(Guid id);
}

View File

@ -1,19 +0,0 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Captcha;
public static class CaptchaStartup
{
public static void AddCaptcha(this IServiceCollection services, VoidSettings settings)
{
if (settings.CaptchaSettings != default)
{
services.AddTransient<ICaptchaVerifier, hCaptchaVerifier>();
}
else
{
services.AddTransient<ICaptchaVerifier, NoOpVerifier>();
}
}
}

View File

@ -1,15 +0,0 @@
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Captcha;
/// <summary>
/// No captcha system is configured
/// </summary>
public class NoOpVerifier : ICaptchaVerifier
{
/// <inheritdoc />
public ValueTask<bool> Verify(string? token)
{
return ValueTask.FromResult(true);
}
}

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