backend complete
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
38
AobaServer/Controllers/Api/MediaApi.cs
Normal file
38
AobaServer/Controllers/Api/MediaApi.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
IsEssential = true,
|
||||||
return View(token);
|
SameSite = SameSiteMode.Strict,
|
||||||
|
Secure = true,
|
||||||
|
});
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
18
AobaServer/Models/ShareXDestination.cs
Normal file
18
AobaServer/Models/ShareXDestination.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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 =>
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user