This commit is contained in:
2025-04-08 13:43:23 -04:00
parent 05afb855be
commit 8803a70a31
14 changed files with 335 additions and 9 deletions

View File

@@ -1,7 +1,5 @@
using AobaV2.Models; using AobaV2.Models;
using MaybeError;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
@@ -16,4 +14,9 @@ public class AobaService(IMongoDatabase db)
{ {
return await _media.Find(m => m.Id == id).FirstOrDefaultAsync(); return await _media.Find(m => m.Id == id).FirstOrDefaultAsync();
} }
public Task AddMediaAsync(Media media)
{
return _media.InsertOneAsync(media);
}
} }

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; global using MaybeError;
using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;

25
AobaCore/MediaService.cs Normal file
View File

@@ -0,0 +1,25 @@
using AobaV2.Models;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.GridFS;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AobaCore;
public class MediaService(IMongoDatabase db, AobaService aobaService)
{
private readonly GridFSBucket _gridFs = new(db);
public async Task<Maybe<Media>> UploadMediaAsync(Stream data, string filename, ObjectId owner, CancellationToken cancellationToken = default)
{
var fileId = await _gridFs.UploadFromStreamAsync(filename, data, cancellationToken: cancellationToken);
var media = new Media(fileId, filename, owner);
await aobaService.AddMediaAsync(media);
return media;
}
}

View File

@@ -9,9 +9,9 @@ public class Media
[BsonId] [BsonId]
public ObjectId Id { get; set; } public ObjectId Id { get; set; }
public ObjectId MediaId { get; set; } public ObjectId MediaId { get; set; }
public required string Filename { get; set; } public string Filename { get; set; }
public MediaType MediaType { get; set; } public MediaType MediaType { get; set; }
public required string Ext { get; set; } public string Ext { get; set; }
public int ViewCount { get; set; } public int ViewCount { get; set; }
public ObjectId Owner { get; set; } public ObjectId Owner { get; set; }
@@ -49,6 +49,23 @@ public class Media
{ ".py", MediaType.Code }, { ".py", MediaType.Code },
}; };
[BsonConstructor]
private Media()
{
Filename = string.Empty;
Ext = string.Empty;
}
public Media(ObjectId fileId, string filename, ObjectId owner)
{
MediaType = GetMediaType(filename);
Ext = Path.GetExtension(filename);
Filename = filename;
MediaId = fileId;
Owner = owner;
Id = ObjectId.GenerateNewId();
}
public string GetMediaUrl() public string GetMediaUrl()
{ {
return this switch return this switch

View File

@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Text.Encodings.Web;
namespace AobaV2;
internal class AobaAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public AobaAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
throw new System.NotImplementedException();
}
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
//Don't challenge API requests
if (OriginalPath.StartsWithSegments("/api"))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
Response.BodyWriter.Complete();
return Task.CompletedTask;
}
//Redirect to login page
Response.Redirect($"/auth/login?ReturnUrl={Uri.EscapeDataString(OriginalPath)}");
return Task.CompletedTask;
}
protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
{
//Don't show error page for api requests
if (OriginalPath.StartsWithSegments("/api"))
{
Response.StatusCode = StatusCodes.Status403Forbidden;
Response.BodyWriter.Complete();
return Task.CompletedTask;
}
//Show Error page
Response.Redirect($"/error/{StatusCodes.Status403Forbidden}");
return Task.CompletedTask;
}
}

View File

@@ -18,6 +18,7 @@
<PackageReference Include="AspNetCore.SassCompiler" Version="1.86.0" /> <PackageReference Include="AspNetCore.SassCompiler" Version="1.86.0" />
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" /> <PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
<PackageReference Include="MaybeError" Version="1.0.5" /> <PackageReference Include="MaybeError" Version="1.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.3" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.7.0" /> <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.7.0" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.8.1"> <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.8.1">

74
AobaV2/AuthInfo.cs Normal file
View File

@@ -0,0 +1,74 @@
using MongoDB.Bson.IO;
using System.Security.Cryptography;
using System.Text.Json;
namespace AobaV2;
public class AuthInfo
{
public required string Issuer;
public required string Audience;
public required byte[] SecureKey;
/// <summary>
/// Save this auth into in a json format to the sepcified file
/// </summary>
/// <param name="path">File path</param>
/// <returns></returns>
public AuthInfo Save(string path)
{
File.WriteAllText(path, JsonSerializer.Serialize(this));
return this;
}
/// <summary>
/// Generate a new Auth Info with newly generated keys
/// </summary>
/// <param name="issuer"></param>
/// <param name="audience"></param>
/// <returns></returns>
public static AuthInfo Create(string issuer, string audience)
{
var auth = new AuthInfo
{
Issuer = issuer,
Audience = audience,
SecureKey = GenetateJWTKey()
};
return auth;
}
/// <summary>
/// Load auth info from a json file
/// </summary>
/// <param name="path">File path</param>
/// <returns></returns>
internal static AuthInfo? Load(string path)
{
return JsonSerializer.Deserialize<AuthInfo>(File.ReadAllText(path));
}
internal static AuthInfo LoadOrCreate(string path, string issuer, string audience)
{
if (File.Exists(path))
{
var loaded = Load(path);
if(loaded != null)
return loaded;
}
var info = Create(issuer, audience);
info.Save(path);
return info;
}
/// <summary>
/// Generate a new key for use by JWT
/// </summary>
/// <returns></returns>
public static byte[] GenetateJWTKey(int size = 64)
{
var key = new byte[size];
RandomNumberGenerator.Fill(key);
return key;
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
namespace AobaV2.Controllers.Api;
[Route("/api/auth")]
public class AuthApi: ControllerBase
{
[HttpGet("login")]
public async Task<IActionResult> LoginAsync()
{
throw new NotImplementedException();
}
[HttpGet("register")]
public async Task<IActionResult> LoginAsync()
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AobaV2.Controllers;
[AllowAnonymous]
[Route("auth")]
public class AuthController : Controller
{
[HttpGet("login")]
public IActionResult Login([FromQuery] string returnUrl)
{
ViewData["returnUrl"] = returnUrl;
return View();
}
[HttpGet("register/{token}")]
public IActionResult Register(string token)
{
return View(token);
}
}

View File

@@ -1,10 +1,28 @@
using Microsoft.AspNetCore.Mvc; using AobaCore;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Bson;
using MongoDB.Driver;
namespace AobaV2.Controllers; namespace AobaV2.Controllers;
public class MediaController : Controller
[Route("/m")]
public class MediaController(MediaService media) : Controller
{ {
public IActionResult Index() [HttpGet("{id}")]
public IActionResult Media(ObjectId id)
{ {
meda
return View(); return View();
} }
[HttpGet("/i/{id}/{*rest}")]
public async Task<IActionResult> LegacyRedirectAsync(ObjectId id, string rest, [FromServices] AobaService aoba)
{
var media = await aoba.GetMediaAsync(id);
if (media == null)
return NotFound();
return LocalRedirectPermanent($"/m/{media.Id}/{rest}");
}
} }

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using MongoDB.Bson;
namespace AobaV2.Models;
public class BsonIdModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(ObjectId))
return new BsonIdModelBinder();
return default;
}
}
public class BsonIdModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == ValueProviderResult.None)
return Task.CompletedTask;
if (ObjectId.TryParse(value.FirstValue, out var id))
bindingContext.Result = ModelBindingResult.Success(id);
else
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
}

View File

@@ -1,17 +1,73 @@
using AobaCore; using AobaCore;
using AobaV2;
using AobaV2.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
#if DEBUG #if DEBUG
builder.Services.AddControllersWithViews().AddRazorRuntimeCompilation(); builder.Services
.AddControllersWithViews(opt => opt.ModelBinderProviders.Add(new BsonIdModelBinderProvider()))
.AddRazorRuntimeCompilation();
builder.Services.AddSassCompiler(); builder.Services.AddSassCompiler();
#else #else
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
#endif #endif
var authInfo = AuthInfo.LoadOrCreate("Auth.json", "aobaV2", "aoba");
builder.Services.AddSingleton(authInfo);
var signingKey = new SymmetricSecurityKey(authInfo.SecureKey);
var validationParams = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateIssuer = true,
ValidIssuer = authInfo.Issuer,
ValidateAudience = true,
ValidAudience = authInfo.Audience,
ValidateLifetime = false,
ClockSkew = TimeSpan.FromMinutes(1),
};
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "Aoba";
}).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => //Bearer auth
{
options.TokenValidationParameters = validationParams;
options.Events = new JwtBearerEvents
{
OnMessageReceived = ctx => //Retreive token from cookie if not found in headers
{
if (string.IsNullOrWhiteSpace(ctx.Token))
ctx.Token = ctx.Request.Cookies["token"];
if (string.IsNullOrWhiteSpace(ctx.Token))
ctx.Token = ctx.Request.Headers["Authorization"].FirstOrDefault()?.Replace("Bearer ", "");
return Task.CompletedTask;
},
OnAuthenticationFailed = ctx =>
{
ctx.Response.Cookies.Append("token", "", new CookieOptions
{
MaxAge = TimeSpan.Zero,
Expires = DateTime.Now
});
ctx.Options.ForwardChallenge = "Aoba";
return Task.CompletedTask;
}
};
}).AddScheme<AuthenticationSchemeOptions, AobaAuthenticationHandler>("Aoba", cfg => { });
builder.Services.AddAoba(); builder.Services.AddAoba();
builder.Services.Configure<FormOptions>(opt => builder.Services.Configure<FormOptions>(opt =>
{ {
@@ -47,6 +103,9 @@ app.Use((c, n) =>
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseRouting(); app.UseRouting();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapStaticAssets(); app.MapStaticAssets();

View File

@@ -0,0 +1,5 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}

View File

@@ -0,0 +1,5 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}