misc
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
using AobaV2.Models;
|
||||
|
||||
using MaybeError;
|
||||
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
|
||||
@@ -16,4 +14,9 @@ public class AobaService(IMongoDatabase db)
|
||||
{
|
||||
return await _media.Find(m => m.Id == id).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public Task AddMediaAsync(Media media)
|
||||
{
|
||||
return _media.InsertOneAsync(media);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
global using MaybeError;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
25
AobaCore/MediaService.cs
Normal file
25
AobaCore/MediaService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@ public class Media
|
||||
[BsonId]
|
||||
public ObjectId Id { get; set; }
|
||||
public ObjectId MediaId { get; set; }
|
||||
public required string Filename { get; set; }
|
||||
public string Filename { get; set; }
|
||||
public MediaType MediaType { get; set; }
|
||||
public required string Ext { get; set; }
|
||||
public string Ext { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public ObjectId Owner { get; set; }
|
||||
|
||||
@@ -49,6 +49,23 @@ public class Media
|
||||
{ ".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()
|
||||
{
|
||||
return this switch
|
||||
|
||||
45
AobaV2/AobaAuthenticationHandler.cs
Normal file
45
AobaV2/AobaAuthenticationHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
<PackageReference Include="AspNetCore.SassCompiler" Version="1.86.0" />
|
||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||
<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.IdentityModel.JsonWebTokens" Version="8.7.0" />
|
||||
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.8.1">
|
||||
|
||||
74
AobaV2/AuthInfo.cs
Normal file
74
AobaV2/AuthInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
19
AobaV2/Controllers/Api/AuthApi.cs
Normal file
19
AobaV2/Controllers/Api/AuthApi.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
23
AobaV2/Controllers/AuthController.cs
Normal file
23
AobaV2/Controllers/AuthController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,28 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AobaCore;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
[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}");
|
||||
}
|
||||
}
|
||||
|
||||
31
AobaV2/Models/BsonIdModelBinderProvider.cs
Normal file
31
AobaV2/Models/BsonIdModelBinderProvider.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,73 @@
|
||||
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.IdentityModel.Tokens;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
#if DEBUG
|
||||
builder.Services.AddControllersWithViews().AddRazorRuntimeCompilation();
|
||||
builder.Services
|
||||
.AddControllersWithViews(opt => opt.ModelBinderProviders.Add(new BsonIdModelBinderProvider()))
|
||||
.AddRazorRuntimeCompilation();
|
||||
builder.Services.AddSassCompiler();
|
||||
#else
|
||||
builder.Services.AddControllersWithViews();
|
||||
#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.Configure<FormOptions>(opt =>
|
||||
{
|
||||
@@ -47,6 +103,9 @@ app.Use((c, n) =>
|
||||
app.UseHttpsRedirection();
|
||||
app.UseRouting();
|
||||
|
||||
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapStaticAssets();
|
||||
|
||||
5
AobaV2/Views/Auth/Login.cshtml
Normal file
5
AobaV2/Views/Auth/Login.cshtml
Normal file
@@ -0,0 +1,5 @@
|
||||
@*
|
||||
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
||||
*@
|
||||
@{
|
||||
}
|
||||
5
AobaV2/Views/Auth/Register.cshtml
Normal file
5
AobaV2/Views/Auth/Register.cshtml
Normal file
@@ -0,0 +1,5 @@
|
||||
@*
|
||||
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
||||
*@
|
||||
@{
|
||||
}
|
||||
Reference in New Issue
Block a user