backend complete

This commit is contained in:
2025-05-17 13:48:10 -04:00
parent 3ac1fbcd8e
commit bb740cbefc
12 changed files with 173 additions and 34 deletions

View File

@@ -22,27 +22,28 @@ public class AccountsService(IMongoDatabase db)
return await _users.Find(u => u.Id == id).FirstOrDefaultAsync(cancellationToken); return await _users.Find(u => u.Id == id).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<bool> VerifyLoginAsync(string username, string password, CancellationToken cancellationToken = default) public async Task<User?> VerifyLoginAsync(string username, string password, CancellationToken cancellationToken = default)
{ {
var user = await _users.Find(u => u.Username == username).FirstOrDefaultAsync(cancellationToken); var user = await _users.Find(u => u.Username == username).FirstOrDefaultAsync(cancellationToken);
if(user.IsArgon) if(user.IsArgon && Argon2.Verify(user.PasswordHash, password))
return Argon2.Verify(user.PasswordHash, password); return user;
if(LegacyVerifyPassword( password, user.PasswordHash)) if(LegacyVerifyPassword( password, user.PasswordHash))
{ {
#if !DEBUG
var argon2Hash = Argon2.Hash(password); var argon2Hash = Argon2.Hash(password);
var update = Builders<User>.Update.Set(u => u.PasswordHash, argon2Hash).Set(u => u.IsArgon, true); var update = Builders<User>.Update.Set(u => u.PasswordHash, argon2Hash).Set(u => u.IsArgon, true);
await _users.UpdateOneAsync(u => u.Id == user.Id, update, cancellationToken: cancellationToken); await _users.UpdateOneAsync(u => u.Id == user.Id, update, cancellationToken: cancellationToken);
return true; #endif
return user;
} }
return false; return null;
} }
public bool LegacyVerifyPassword(string password, string passwordHash) public static bool LegacyVerifyPassword(string password, string passwordHash)
{ {
if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(passwordHash)) if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(passwordHash))
return false; return false;

View File

@@ -8,11 +8,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" /> <PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.4" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
<PackageReference Include="MaybeError" Version="1.1.0" /> <PackageReference Include="MaybeError" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" />
<PackageReference Include="MongoDB.Analyzer" Version="1.5.0" /> <PackageReference Include="MongoDB.Analyzer" Version="2.0.0" />
<PackageReference Include="MongoDB.Driver" Version="3.3.0" /> <PackageReference Include="MongoDB.Driver" Version="3.4.0" />
<PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.0.0" /> <PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.10.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.10.0" />
</ItemGroup> </ItemGroup>

View File

@@ -77,8 +77,9 @@ public class AobaService(IMongoDatabase db)
{ {
try try
{ {
await _gridFs.DeleteAsync(fileId, cancellationToken); cancellationToken.ThrowIfCancellationRequested();
await _media.DeleteOneAsync(m => m.MediaId == fileId, cancellationToken); await _gridFs.DeleteAsync(fileId, CancellationToken.None);
await _media.DeleteOneAsync(m => m.MediaId == fileId, CancellationToken.None);
} }
catch (GridFSFileNotFoundException) catch (GridFSFileNotFoundException)
{ {

View File

@@ -23,6 +23,7 @@ public static class Extensions
services.AddSingleton(dbClient); services.AddSingleton(dbClient);
services.AddSingleton<IMongoDatabase>(db); services.AddSingleton<IMongoDatabase>(db);
services.AddSingleton<AobaService>(); services.AddSingleton<AobaService>();
services.AddSingleton<AccountsService>();
services.AddHostedService<AobaIndexCreationService>(); services.AddHostedService<AobaIndexCreationService>();
return services; return services;
} }

View File

@@ -32,4 +32,5 @@ public class User
id.AddClaim(new Claim(ClaimTypes.Role, Role)); id.AddClaim(new Claim(ClaimTypes.Role, Role));
return id; return id;
} }
} }

View File

@@ -11,20 +11,20 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" /> <PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
<PackageReference Include="Grpc.AspNetCore.Web" Version="2.71.0" /> <PackageReference Include="Grpc.AspNetCore.Web" Version="2.71.0" />
<PackageReference Include="Grpc.Tools" Version="2.71.0"> <PackageReference Include="Grpc.Tools" Version="2.72.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" /> <PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.9.0" /> <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.10.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
<PackageReference Include="MimeTypesMap" Version="1.0.9" /> <PackageReference Include="MimeTypesMap" Version="1.0.9" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" /> <PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" /> <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.1" /> <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.1" /> <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,38 @@
using AobaCore;
using AobaCore.Models;
using AobaServer.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Bson;
namespace AobaServer.Controllers.Api;
[ApiController, Authorize]
[Route("/api/media")]
public class MediaApi(AobaService aoba) : ControllerBase
{
[HttpPost("upload")]
public async Task<IActionResult> UploadAsync([FromForm] IFormFile file, CancellationToken cancellationToken)
{
var media = await aoba.UploadFileAsync(file.OpenReadStream(), file.FileName, User.GetId(), cancellationToken);
if (media.HasError)
return Problem(detail: media.Error.Message, statusCode: StatusCodes.Status400BadRequest);
return Ok(new
{
media.Value,
url = media.Value.GetMediaUrl()
});
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(ObjectId id, CancellationToken cancellationToken)
{
await aoba.DeleteFileAsync(id, cancellationToken);
return Ok();
}
}

View File

@@ -1,23 +1,37 @@
using Microsoft.AspNetCore.Authorization; using AobaCore;
using AobaServer.Models;
using AobaServer.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Net;
namespace AobaServer.Controllers; namespace AobaServer.Controllers;
//allow login via http during debug testing
#if DEBUG
[AllowAnonymous] [AllowAnonymous]
[Route("auth")] [Route("auth")]
public class AuthController : Controller public class AuthController(AccountsService accountsService, AuthInfo authInfo) : Controller
{ {
[HttpGet("login")] [HttpPost("login")]
public IActionResult Login([FromQuery] string returnUrl) public async Task<IActionResult> Login([FromForm] string username, [FromForm] string password, CancellationToken cancellationToken)
{ {
ViewData["returnUrl"] = returnUrl; var user = await accountsService.VerifyLoginAsync(username, password, cancellationToken);
return View();
}
[HttpGet("register/{token}")] if (user == null)
public IActionResult Register(string token) return Problem("Invalid login Credentials", statusCode: StatusCodes.Status400BadRequest);
{ Response.Cookies.Append("token", user.GetToken(authInfo), new CookieOptions
{
return View(token); IsEssential = true,
SameSite = SameSiteMode.Strict,
Secure = true,
});
return Ok();
} }
} }
#endif

View File

@@ -0,0 +1,18 @@
namespace AobaServer.Models;
public class ShareXDestination
{
public string Version { get; set; } = "13.1.0";
public string Name { get; set; } = "Aoba";
public string DestinationType { get; set; } = "ImageUploader, TextUploader, FileUploader";
public string RequestMethod { get; set; } = "POST";
public string RequestURL { get; set; } = "https://aoba.app/api/media/upload";
public Dictionary<string, string> Headers { get; set; } = [];
public string Body { get; set; } = "MultipartFormData";
public Dictionary<string, string> Arguments { get; set; } = new() { { "name", "$filename$" } };
public string FileFormName { get; set; } = "file";
public string[] RegexList { get; set; } = ["([^/]+)/?$"];
public string URL { get; set; } = "https://aoba.app$json:url$";
public required string ThumbnailURL { get; set; }
public required string DeletionURL { get; set; }
}

View File

@@ -68,6 +68,12 @@ builder.Services.AddAuthentication(options =>
{ {
if (string.IsNullOrWhiteSpace(ctx.Token)) if (string.IsNullOrWhiteSpace(ctx.Token))
ctx.Token = ctx.Request.Headers.Authorization.FirstOrDefault()?.Replace("Bearer ", ""); ctx.Token = ctx.Request.Headers.Authorization.FirstOrDefault()?.Replace("Bearer ", "");
#if DEBUG //allow cookie based auth when in debug mode
if(string.IsNullOrWhiteSpace(ctx.Token))
ctx.Token = ctx.Request.Cookies.FirstOrDefault(c => c.Key == "token").Value;
#endif
return Task.CompletedTask; return Task.CompletedTask;
}, },
OnAuthenticationFailed = ctx => OnAuthenticationFailed = ctx =>

View File

@@ -1,6 +1,43 @@
namespace AobaServer.Services; using Aoba.RPC.Auth;
public class AobaAuthService() : Aoba.RPC.Auth.AuthRpc.AuthRpcBase using AobaCore;
using AobaCore.Models;
using AobaServer.Models;
using AobaServer.Utils;
using Grpc.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
namespace AobaServer.Services;
public class AobaAuthService(AccountsService accountsService, AuthInfo authInfo) : Aoba.RPC.Auth.AuthRpc.AuthRpcBase
{ {
[AllowAnonymous]
public override async Task<LoginResponse> Login(Credentials request, ServerCallContext context)
{
var user = await accountsService.VerifyLoginAsync(request.User, request.Password, context.CancellationToken);
if (user == null)
return new LoginResponse
{
Error = new LoginError
{
Message = "Invalid login credentials"
}
};
var token = user.GetToken(authInfo);
return new LoginResponse
{
Jwt = new Jwt
{
Token = token
}
};
}
} }

View File

@@ -1,6 +1,15 @@
using MongoDB.Bson; using AobaCore.Models;
using AobaServer.Models;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
namespace AobaServer.Utils; namespace AobaServer.Utils;
public static class Extensions public static class Extensions
@@ -14,5 +23,18 @@ public static class Extensions
return ObjectId.Empty; return ObjectId.Empty;
} }
public static string GetToken(this User user, AuthInfo authInfo)
{
var handler = new JwtSecurityTokenHandler();
var signCreds = new SigningCredentials(new SymmetricSecurityKey(authInfo.SecureKey), SecurityAlgorithms.HmacSha256);
var identity = user.GetIdentity();
var token = handler.CreateEncodedJwt(authInfo.Issuer, authInfo.Audience, identity, notBefore: DateTime.Now, expires: null, issuedAt: DateTime.Now, signCreds);
return token;
}
public static ObjectId GetId(this ClaimsPrincipal user)
{
return user.FindFirstValue(ClaimTypes.NameIdentifier).ToObjectId();
}
} }