Compare commits

...

5 Commits

Author SHA1 Message Date
38b7e8c1a1 🐛 Bug fixes 2025-04-25 23:13:15 +08:00
c8e9f73746 🐛 Fix unable to inject http client factory 2025-04-23 22:17:02 +08:00
a008a74d77 Recaptcha 2025-04-23 01:18:12 +08:00
31db3d5388 🐛 Fix bugs 2025-04-23 00:30:25 +08:00
c43ff6be7b 🐛 Bug fixes on posts and publishers 2025-04-19 23:51:27 +08:00
14 changed files with 351 additions and 28 deletions

View File

@ -16,7 +16,12 @@ public class AccountController(AppDatabase db, FileService fs, IMemoryCache memC
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Account?>> GetByName(string name) public async Task<ActionResult<Account?>> GetByName(string name)
{ {
var account = await db.Accounts.Where(a => a.Name == name).FirstOrDefaultAsync(); var account = await db.Accounts
.Include(e => e.Profile)
.Include(e => e.Profile.Picture)
.Include(e => e.Profile.Background)
.Where(a => a.Name == name)
.FirstOrDefaultAsync();
return account is null ? new NotFoundResult() : account; return account is null ? new NotFoundResult() : account;
} }

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
using NodaTime; using NodaTime;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Text.Json;
namespace DysonNetwork.Sphere.Auth; namespace DysonNetwork.Sphere.Auth;
@ -14,7 +15,8 @@ public class AuthController(
AppDatabase db, AppDatabase db,
AccountService accounts, AccountService accounts,
AuthService auth, AuthService auth,
IHttpContextAccessor httpContext IConfiguration configuration,
IHttpClientFactory httpClientFactory
) : ControllerBase ) : ControllerBase
{ {
public class ChallengeRequest public class ChallengeRequest
@ -31,8 +33,8 @@ public class AuthController(
var account = await accounts.LookupAccount(request.Account); var account = await accounts.LookupAccount(request.Account);
if (account is null) return NotFound("Account was not found."); if (account is null) return NotFound("Account was not found.");
var ipAddress = httpContext.HttpContext?.Connection.RemoteIpAddress?.ToString(); var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
var userAgent = httpContext.HttpContext?.Request.Headers.UserAgent.ToString(); var userAgent = HttpContext.Request.Headers.UserAgent.ToString();
var now = Instant.FromDateTimeUtc(DateTime.UtcNow); var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
@ -75,6 +77,23 @@ public class AuthController(
: challenge.Account.AuthFactors.ToList(); : challenge.Account.AuthFactors.ToList();
} }
[HttpPost("challenge/{id}/factors/{factorId:long}")]
public async Task<ActionResult> RequestFactorCode([FromRoute] Guid id, [FromRoute] long factorId)
{
var challenge = await db.AuthChallenges
.Include(e => e.Account)
.Where(e => e.Id == id).FirstOrDefaultAsync();
if (challenge is null) return NotFound("Auth challenge was not found.");
var factor = await db.AccountAuthFactors
.Where(e => e.Id == factorId)
.Where(e => e.Account == challenge.Account).FirstOrDefaultAsync();
if (factor is null) return NotFound("Auth factor was not found.");
// TODO do the logic here
return Ok();
}
public class PerformChallengeRequest public class PerformChallengeRequest
{ {
[Required] public long FactorId { get; set; } [Required] public long FactorId { get; set; }
@ -186,7 +205,7 @@ public class AuthController(
[HttpGet("test")] [HttpGet("test")]
public async Task<ActionResult> Test() public async Task<ActionResult> Test()
{ {
var sessionIdClaim = httpContext.HttpContext?.User.FindFirst("session_id")?.Value; var sessionIdClaim = HttpContext.User.FindFirst("session_id")?.Value;
if (!Guid.TryParse(sessionIdClaim, out var sessionId)) if (!Guid.TryParse(sessionIdClaim, out var sessionId))
return Unauthorized(); return Unauthorized();
@ -195,4 +214,53 @@ public class AuthController(
return Ok(session); return Ok(session);
} }
[HttpPost("captcha")]
public async Task<ActionResult> ValidateCaptcha([FromBody] string token)
{
var provider = configuration.GetSection("Captcha")["Provider"]?.ToLower();
var apiKey = configuration.GetSection("Captcha")["ApiKey"];
var apiSecret = configuration.GetSection("Captcha")["ApiSecret"];
var client = httpClientFactory.CreateClient();
switch (provider)
{
case "cloudflare":
var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
"application/x-www-form-urlencoded");
var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify",
content);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var cfResult = JsonSerializer.Deserialize<CloudflareVerificationResponse>(json);
if (cfResult?.Success == true)
return Ok(new { success = true });
return BadRequest(new { success = false, errors = cfResult?.ErrorCodes });
case "google":
var secretKey = configuration.GetSection("CaptchaSettings")["GoogleRecaptchaSecretKey"];
if (string.IsNullOrEmpty(secretKey))
{
return StatusCode(500, "Google reCaptcha secret key is not configured.");
}
content = new StringContent($"secret={secretKey}&response={token}", System.Text.Encoding.UTF8,
"application/x-www-form-urlencoded");
response = await client.PostAsync("https://www.google.com/recaptcha/api/siteverify", content);
response.EnsureSuccessStatusCode();
json = await response.Content.ReadAsStringAsync();
var capResult = JsonSerializer.Deserialize<GoogleVerificationResponse>(json);
if (capResult?.Success == true)
return Ok(new { success = true });
return BadRequest(new { success = false, errors = capResult?.ErrorCodes });
default:
return StatusCode(500, "The server misconfigured for the captcha.");
}
}
} }

View File

@ -0,0 +1,17 @@
namespace DysonNetwork.Sphere.Auth;
public class CloudflareVerificationResponse
{
public bool Success { get; set; }
public string[]? ErrorCodes { get; set; }
}
public class GoogleVerificationResponse
{
public bool Success { get; set; }
public float Score { get; set; }
public string Action { get; set; }
public DateTime ChallengeTs { get; set; }
public string Hostname { get; set; }
public string[]? ErrorCodes { get; set; }
}

View File

@ -14,6 +14,8 @@ public class UserInfoMiddleware(RequestDelegate next, IMemoryCache cache)
{ {
user = await db.Accounts user = await db.Accounts
.Include(e => e.Profile) .Include(e => e.Profile)
.Include(e => e.Profile.Picture)
.Include(e => e.Profile.Background)
.Where(e => e.Id == userId) .Where(e => e.Id == userId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
@ -26,6 +28,8 @@ public class UserInfoMiddleware(RequestDelegate next, IMemoryCache cache)
if (user is not null) if (user is not null)
{ {
context.Items["CurrentUser"] = user; context.Items["CurrentUser"] = user;
var prefix = user.IsSuperuser ? "super:" : "";
context.Items["CurrentIdentity"] = $"{prefix}{userId}";
} }
} }

View File

@ -11,4 +11,4 @@ g = _, _, _
e = some(where (p.eft == allow)) e = some(where (p.eft == allow))
[matchers] [matchers]
m = r.sub.StartsWith("super:") || (g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act) m = regexMatch(r.sub, "^super:") || (g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act)

View File

@ -0,0 +1,149 @@
@page "/auth/captcha"
@model DysonNetwork.Sphere.Pages.CheckpointPage
@{
Layout = null;
var cfg = ViewData.Model.Configuration;
var provider = cfg.GetSection("Captcha")["Provider"]?.ToLower();
var apiKey = cfg.GetSection("Captcha")["ApiKey"];
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Solar Network Captcha</title>
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap"
rel="stylesheet"
/>
<style>
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #2d2d2d;
font-family: "Roboto Mono", monospace;
color: #c9d1d9;
}
.parent {
padding: 20px;
max-width: 480px;
margin: 0 auto;
}
h2 {
font-size: 18px;
font-weight: 300;
color: #ffffff;
margin-bottom: 15px;
}
.footer {
margin-top: 20px;
font-size: 11px;
opacity: 0.6;
}
.footer-product {
font-size: 12px;
font-weight: bold;
margin-bottom: 5px;
opacity: 0.8;
}
.g-recaptcha {
display: inline-block; /* Adjust as needed */
}
</style>
@switch (provider)
{
case "recaptcha":
<script src="https://www.recaptcha.net/recaptcha/api.js" async defer></script>
break;
case "cloudflare":
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
></script>
break;
}
</head>
<body>
<div class="parent">
<div class="container">
<h1>reCaptcha</h1>
@switch (provider)
{
case "cloudflare":
<div
class="cf-turnstile"
data-sitekey="@apiKey"
data-callback="onSuccess"
></div>
break;
case "recaptcha":
<div
class="g-recaptcha"
data-sitekey="@apiKey"
data-callback="onSuccess"
></div>
break;
default:
<p style="color: yellow;">Captcha provider not configured correctly.</p>
break;
}
</div>
<div class="footer">
<div class="footer-product">Solar Network Anti-Robot</div>
<a
href="https://solsynth.dev"
style="color: #c9d1d9; text-decoration: none"
>Solsynth LLC</a>
&copy; @DateTime.Now.Year<br/>
Powered by
@switch (provider)
{
case "cloudflare":
<a href="https://www.cloudflare.com/turnstile/" style="color: #c9d1d9"
>Cloudflare Turnstile</a>
break;
case "recaptcha":
<a href="https://www.google.com/recaptcha/" style="color: #c9d1d9"
>Google reCaptcha</a>
break;
default:
<span>Nothing</span>
break;
}
<br/>
Hosted by
<a
@* TODO Update the project link here *@
href="https://github.com/Solsynth/HyperNet.Nexus"
style="color: #c9d1d9"
>DysonNetwork.Sphere</a>
</div>
</div>
<script>
function getQueryParam(name) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
function onSuccess(token) {
window.parent.postMessage("captcha_tk=" + token, "*");
const redirectUri = getQueryParam("redirect_uri");
if (redirectUri) {
window.location.href = `${redirectUri}?captcha_tk=${encodeURIComponent(token)}`;
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace DysonNetwork.Sphere.Pages;
public class CheckpointPage(IConfiguration configuration) : PageModel
{
[BindProperty] public IConfiguration Configuration { get; set; } = configuration;
public void OnGet()
{
}
}

View File

@ -1,12 +1,14 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Casbin;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Post; namespace DysonNetwork.Sphere.Post;
[ApiController] [ApiController]
[Route("/posts")] [Route("/posts")]
public class PostController(AppDatabase db, PostService ps) : ControllerBase public class PostController(AppDatabase db, PostService ps, IEnforcer enforcer) : ControllerBase
{ {
[HttpGet] [HttpGet]
public async Task<ActionResult<List<Post>>> ListPosts([FromQuery] int offset = 0, [FromQuery] int take = 20) public async Task<ActionResult<List<Post>>> ListPosts([FromQuery] int offset = 0, [FromQuery] int take = 20)
@ -18,12 +20,15 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
.CountAsync(); .CountAsync();
var posts = await db.Posts var posts = await db.Posts
.Include(e => e.Publisher) .Include(e => e.Publisher)
.Include(e => e.Publisher.Picture)
.Include(e => e.Publisher.Background)
.Include(e => e.ThreadedPost) .Include(e => e.ThreadedPost)
.Include(e => e.ForwardedPost) .Include(e => e.ForwardedPost)
.Include(e => e.Attachments) .Include(e => e.Attachments)
.Include(e => e.Categories) .Include(e => e.Categories)
.Include(e => e.Tags) .Include(e => e.Tags)
.FilterWithVisibility(currentUser, isListing: true) .FilterWithVisibility(currentUser, isListing: true)
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
.Skip(offset) .Skip(offset)
.Take(take) .Take(take)
.ToListAsync(); .ToListAsync();
@ -42,6 +47,8 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
var post = await db.Posts var post = await db.Posts
.Where(e => e.Id == id) .Where(e => e.Id == id)
.Include(e => e.Publisher) .Include(e => e.Publisher)
.Include(e => e.Publisher.Picture)
.Include(e => e.Publisher.Background)
.Include(e => e.RepliedPost) .Include(e => e.RepliedPost)
.Include(e => e.ThreadedPost) .Include(e => e.ThreadedPost)
.Include(e => e.ForwardedPost) .Include(e => e.ForwardedPost)
@ -73,12 +80,15 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
var posts = await db.Posts var posts = await db.Posts
.Where(e => e.RepliedPostId == id) .Where(e => e.RepliedPostId == id)
.Include(e => e.Publisher) .Include(e => e.Publisher)
.Include(e => e.Publisher.Picture)
.Include(e => e.Publisher.Background)
.Include(e => e.ThreadedPost) .Include(e => e.ThreadedPost)
.Include(e => e.ForwardedPost) .Include(e => e.ForwardedPost)
.Include(e => e.Attachments) .Include(e => e.Attachments)
.Include(e => e.Categories) .Include(e => e.Categories)
.Include(e => e.Tags) .Include(e => e.Tags)
.FilterWithVisibility(currentUser, isListing: true) .FilterWithVisibility(currentUser, isListing: true)
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
.Skip(offset) .Skip(offset)
.Take(take) .Take(take)
.ToListAsync(); .ToListAsync();
@ -99,6 +109,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
[MaxLength(8)] public List<string>? Categories { get; set; } [MaxLength(8)] public List<string>? Categories { get; set; }
[MaxLength(32)] public List<string>? Attachments { get; set; } [MaxLength(32)] public List<string>? Attachments { get; set; }
public Dictionary<string, object>? Meta { get; set; } public Dictionary<string, object>? Meta { get; set; }
public Instant? PublishedAt { get; set; }
} }
[HttpPost] [HttpPost]
@ -108,6 +119,8 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
) )
{ {
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (!await enforcer.EnforceAsync((string)HttpContext.Items["CurrentIdentity"]!, "global", "posts", "create"))
return StatusCode(403);
Publisher? publisher; Publisher? publisher;
if (publisherName is null) if (publisherName is null)
@ -136,8 +149,10 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
Description = request.Description, Description = request.Description,
Content = request.Content, Content = request.Content,
Visibility = request.Visibility ?? PostVisibility.Public, Visibility = request.Visibility ?? PostVisibility.Public,
PublishedAt = request.PublishedAt,
Type = request.Type ?? PostType.Moment, Type = request.Type ?? PostType.Moment,
Meta = request.Meta, Meta = request.Meta,
Publisher = publisher,
}; };
try try
@ -165,6 +180,8 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
var post = await db.Posts var post = await db.Posts
.Where(e => e.Id == id) .Where(e => e.Id == id)
.Include(e => e.Publisher) .Include(e => e.Publisher)
.Include(e => e.Publisher.Picture)
.Include(e => e.Publisher.Background)
.Include(e => e.Attachments) .Include(e => e.Attachments)
.Include(e => e.Categories) .Include(e => e.Categories)
.Include(e => e.Tags) .Include(e => e.Tags)
@ -190,7 +207,8 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
post, post,
attachments: request.Attachments, attachments: request.Attachments,
tags: request.Tags, tags: request.Tags,
categories: request.Categories categories: request.Categories,
publishedAt: request.PublishedAt
); );
} }
catch (InvalidOperationException err) catch (InvalidOperationException err)

View File

@ -13,6 +13,16 @@ public class PostService(AppDatabase db, FileService fs)
List<string>? categories = null List<string>? categories = null
) )
{ {
if (post.PublishedAt is not null)
{
if (post.PublishedAt.Value.ToDateTimeUtc() < DateTime.UtcNow)
throw new InvalidOperationException("Cannot create the post which published in the past.");
}
else
{
post.PublishedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
}
if (attachments is not null) if (attachments is not null)
{ {
post.Attachments = await db.Files.Where(e => attachments.Contains(e.Id)).ToListAsync(); post.Attachments = await db.Files.Where(e => attachments.Contains(e.Id)).ToListAsync();
@ -46,7 +56,7 @@ public class PostService(AppDatabase db, FileService fs)
if (post.Categories.Count != categories.Distinct().Count()) if (post.Categories.Count != categories.Distinct().Count())
throw new InvalidOperationException("Categories contains one or more categories that wasn't exists."); throw new InvalidOperationException("Categories contains one or more categories that wasn't exists.");
} }
if (post.Empty) if (post.Empty)
throw new InvalidOperationException("Cannot create a post with barely no content."); throw new InvalidOperationException("Cannot create a post with barely no content.");
@ -63,11 +73,21 @@ public class PostService(AppDatabase db, FileService fs)
Post post, Post post,
List<string>? attachments = null, List<string>? attachments = null,
List<string>? tags = null, List<string>? tags = null,
List<string>? categories = null List<string>? categories = null,
Instant? publishedAt = null
) )
{ {
post.EditedAt = Instant.FromDateTimeUtc(DateTime.UtcNow); post.EditedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
if (publishedAt is not null)
{
// User cannot set the published at to the past to prevent scam,
// But we can just let the controller set the published at, because when no changes to
// the published at will blocked the update operation
if (publishedAt.Value.ToDateTimeUtc() < DateTime.UtcNow)
throw new InvalidOperationException("Cannot set the published at to the past.");
}
if (attachments is not null) if (attachments is not null)
{ {
var records = await db.Files.Where(e => attachments.Contains(e.Id)).ToListAsync(); var records = await db.Files.Where(e => attachments.Contains(e.Id)).ToListAsync();
@ -87,7 +107,7 @@ public class PostService(AppDatabase db, FileService fs)
await fs.MarkUsageRangeAsync(added, 1); await fs.MarkUsageRangeAsync(added, 1);
await fs.MarkUsageRangeAsync(removed, -1); await fs.MarkUsageRangeAsync(removed, -1);
} }
if (tags is not null) if (tags is not null)
{ {
var existingTags = await db.PostTags.Where(e => tags.Contains(e.Slug)).ToListAsync(); var existingTags = await db.PostTags.Where(e => tags.Contains(e.Slug)).ToListAsync();
@ -111,14 +131,14 @@ public class PostService(AppDatabase db, FileService fs)
post.Categories = await db.PostCategories.Where(e => categories.Contains(e.Slug)).ToListAsync(); post.Categories = await db.PostCategories.Where(e => categories.Contains(e.Slug)).ToListAsync();
if (post.Categories.Count != categories.Distinct().Count()) if (post.Categories.Count != categories.Distinct().Count())
throw new InvalidOperationException("Categories contains one or more categories that wasn't exists."); throw new InvalidOperationException("Categories contains one or more categories that wasn't exists.");
} }
if (post.Empty) if (post.Empty)
throw new InvalidOperationException("Cannot edit a post to barely no content."); throw new InvalidOperationException("Cannot edit a post to barely no content.");
db.Update(post); db.Update(post);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return post; return post;
} }

View File

@ -20,6 +20,8 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
var publisher = await db.Publishers var publisher = await db.Publishers
.Where(e => e.Name == name) .Where(e => e.Name == name)
.Include(e => e.Picture)
.Include(e => e.Background)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (publisher is null) return NotFound(); if (publisher is null) return NotFound();
@ -37,6 +39,8 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
.Where(m => m.AccountId == userId) .Where(m => m.AccountId == userId)
.Where(m => m.JoinedAt != null) .Where(m => m.JoinedAt != null)
.Include(e => e.Publisher) .Include(e => e.Publisher)
.Include(e => e.Publisher.Picture)
.Include(e => e.Publisher.Background)
.ToListAsync(); .ToListAsync();
return members.Select(m => m.Publisher).ToList(); return members.Select(m => m.Publisher).ToList();
@ -53,6 +57,8 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
.Where(m => m.AccountId == userId) .Where(m => m.AccountId == userId)
.Where(m => m.JoinedAt == null) .Where(m => m.JoinedAt == null)
.Include(e => e.Publisher) .Include(e => e.Publisher)
.Include(e => e.Publisher.Picture)
.Include(e => e.Publisher.Background)
.ToListAsync(); .ToListAsync();
return members.ToList(); return members.ToList();
@ -164,7 +170,8 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
public async Task<ActionResult<Publisher>> CreatePublisherIndividual(PublisherRequest request) public async Task<ActionResult<Publisher>> CreatePublisherIndividual(PublisherRequest request)
{ {
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (!await enforcer.EnforceAsync(currentUser.Id.ToString(), "global", "publishers", "create")) if (!await enforcer.EnforceAsync((string)HttpContext.Items["CurrentIdentity"]!, "global", "publishers",
"create"))
return StatusCode(403); return StatusCode(403);
var takenName = request.Name ?? currentUser.Name; var takenName = request.Name ?? currentUser.Name;

View File

@ -26,12 +26,14 @@ using File = System.IO.File;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Host.UseContentRoot(Directory.GetCurrentDirectory()); builder.Host.UseContentRoot(Directory.GetCurrentDirectory());
builder.WebHost.ConfigureKestrel(options => options.Limits.MaxRequestBodySize = long.MaxValue);
// Add services to the container. // Add services to the container.
builder.Services.AddDbContext<AppDatabase>(); builder.Services.AddDbContext<AppDatabase>();
builder.Services.AddMemoryCache(); builder.Services.AddMemoryCache();
builder.Services.AddHttpClient();
builder.Services.AddControllers().AddJsonOptions(options => builder.Services.AddControllers().AddJsonOptions(options =>
{ {
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
@ -39,7 +41,7 @@ builder.Services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
}); });
builder.Services.AddHttpContextAccessor(); builder.Services.AddRazorPages();
// Casbin permissions // Casbin permissions
@ -51,6 +53,7 @@ var casbinDbContext = new CasbinDbContext<int>(
var casbinEfcore = new EFCoreAdapter<int>(casbinDbContext); var casbinEfcore = new EFCoreAdapter<int>(casbinDbContext);
casbinDbContext.Database.EnsureCreated(); casbinDbContext.Database.EnsureCreated();
var casbinEncofcer = new Enforcer("Casbin.conf", casbinEfcore); var casbinEncofcer = new Enforcer("Casbin.conf", casbinEfcore);
casbinEncofcer.EnableCache(true);
casbinEncofcer.LoadPolicy(); casbinEncofcer.LoadPolicy();
builder.Services.AddSingleton<IEnforcer>(casbinEncofcer); builder.Services.AddSingleton<IEnforcer>(casbinEncofcer);
@ -153,7 +156,7 @@ using (var scope = app.Services.CreateScope())
db.Database.Migrate(); db.Database.Migrate();
} }
if (app.Environment.IsDevelopment()) app.MapOpenApi(); app.MapOpenApi();
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(); app.UseSwaggerUI();
@ -165,6 +168,8 @@ app.UseForwardedHeaders(new ForwardedHeadersOptions
app.UseCors(opts => app.UseCors(opts =>
opts.SetIsOriginAllowed(_ => true) opts.SetIsOriginAllowed(_ => true)
.WithExposedHeaders("X-Total")
.WithHeaders("X-Total")
.AllowCredentials() .AllowCredentials()
.AllowAnyHeader() .AllowAnyHeader()
.AllowAnyMethod() .AllowAnyMethod()
@ -175,6 +180,8 @@ app.UseAuthorization();
app.UseMiddleware<UserInfoMiddleware>(); app.UseMiddleware<UserInfoMiddleware>();
app.MapControllers(); app.MapControllers();
app.MapStaticAssets();
app.MapRazorPages();
var tusDiskStore = new tusdotnet.Stores.TusDiskStore( var tusDiskStore = new tusdotnet.Stores.TusDiskStore(
builder.Configuration.GetSection("Tus").GetValue<string>("StorePath")! builder.Configuration.GetSection("Tus").GetValue<string>("StorePath")!

View File

@ -81,17 +81,25 @@ public class FileService(AppDatabase db, IConfiguration configuration)
break; break;
case "video": case "video":
case "audio": case "audio":
var mediaInfo = await FFProbe.AnalyseAsync(stream); try
file.FileMeta = new Dictionary<string, object>
{ {
["duration"] = mediaInfo.Duration.TotalSeconds, var mediaInfo = await FFProbe.AnalyseAsync(stream);
["format_name"] = mediaInfo.Format.FormatName, file.FileMeta = new Dictionary<string, object>
["format_long_name"] = mediaInfo.Format.FormatLongName, {
["start_time"] = mediaInfo.Format.StartTime.ToString(), ["duration"] = mediaInfo.Duration.TotalSeconds,
["bit_rate"] = mediaInfo.Format.BitRate.ToString(CultureInfo.InvariantCulture), ["format_name"] = mediaInfo.Format.FormatName,
["tags"] = mediaInfo.Format.Tags ?? [], ["format_long_name"] = mediaInfo.Format.FormatLongName,
["chapters"] = mediaInfo.Chapters, ["start_time"] = mediaInfo.Format.StartTime.ToString(),
}; ["bit_rate"] = mediaInfo.Format.BitRate.ToString(CultureInfo.InvariantCulture),
["tags"] = mediaInfo.Format.Tags ?? [],
["chapters"] = mediaInfo.Chapters,
};
}
catch
{
// ignored
}
break; break;
} }

View File

@ -44,5 +44,10 @@
"EnableSsl": true "EnableSsl": true
} }
] ]
},
"Captcha": {
"Provider": "recaptcha",
"ApiKey": "6LfIzSArAAAAAN413MtycDcPlKa636knBSAhbzj-",
"ApiSecret": ""
} }
} }

View File

@ -11,6 +11,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADirectory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fde_003F94973e27_003FDirectory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADirectory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fde_003F94973e27_003FDirectory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEndpointConventionBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F8a_003F101938e3_003FEndpointConventionBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEndpointConventionBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F8a_003F101938e3_003FEndpointConventionBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcerExtension_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003Fb5_003F180850e0_003FEnforcerExtension_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcerExtension_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003Fb5_003F180850e0_003FEnforcerExtension_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003F47_003F3a6b6c4b_003FEnforcer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe096e6f12c5d6b49356bc34ff1ea08738f910c0929c9d717c9cba7f44288_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe096e6f12c5d6b49356bc34ff1ea08738f910c0929c9d717c9cba7f44288_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003F55_003F277f2d4c_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003F55_003F277f2d4c_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4a28847852ee9ba45fd3107526c0a749a733bd4f4ebf33aa3c9a59737a3f758_003FEntityFrameworkServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4a28847852ee9ba45fd3107526c0a749a733bd4f4ebf33aa3c9a59737a3f758_003FEntityFrameworkServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@ -24,6 +25,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIntentType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fbf_003Ffcb84131_003FIntentType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIntentType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fbf_003Ffcb84131_003FIntentType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIServiceCollectionQuartzConfigurator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003F67_003Faee36f5b_003FIServiceCollectionQuartzConfigurator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIServiceCollectionQuartzConfigurator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003F67_003Faee36f5b_003FIServiceCollectionQuartzConfigurator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AITusStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fb1_003F7e861de5_003FITusStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AITusStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fb1_003F7e861de5_003FITusStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKestrelServerLimits_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1e2e5dfcafad4407b569dd5df56a2fbf274e00_003Fa4_003F39445f62_003FKestrelServerLimits_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMediaAnalysis_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fd7_003F5c138865_003FMediaAnalysis_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMediaAnalysis_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fd7_003F5c138865_003FMediaAnalysis_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMicrosoftDependencyInjectionJobFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003Fa8_003F91b091de_003FMicrosoftDependencyInjectionJobFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMicrosoftDependencyInjectionJobFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003Fa8_003F91b091de_003FMicrosoftDependencyInjectionJobFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFoundResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F28_003F290250f5_003FNotFoundResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFoundResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F28_003F290250f5_003FNotFoundResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>