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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if(user.IsArgon)
|
||||
return Argon2.Verify(user.PasswordHash, password);
|
||||
if(user.IsArgon && Argon2.Verify(user.PasswordHash, password))
|
||||
return user;
|
||||
|
||||
if(LegacyVerifyPassword( password, user.PasswordHash))
|
||||
{
|
||||
#if !DEBUG
|
||||
var argon2Hash = Argon2.Hash(password);
|
||||
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);
|
||||
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))
|
||||
return false;
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
|
||||
<PackageReference Include="MongoDB.Analyzer" Version="1.5.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" />
|
||||
<PackageReference Include="MongoDB.Analyzer" Version="2.0.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.4.0" />
|
||||
<PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -77,8 +77,9 @@ public class AobaService(IMongoDatabase db)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _gridFs.DeleteAsync(fileId, cancellationToken);
|
||||
await _media.DeleteOneAsync(m => m.MediaId == fileId, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await _gridFs.DeleteAsync(fileId, CancellationToken.None);
|
||||
await _media.DeleteOneAsync(m => m.MediaId == fileId, CancellationToken.None);
|
||||
}
|
||||
catch (GridFSFileNotFoundException)
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ public static class Extensions
|
||||
services.AddSingleton(dbClient);
|
||||
services.AddSingleton<IMongoDatabase>(db);
|
||||
services.AddSingleton<AobaService>();
|
||||
services.AddSingleton<AccountsService>();
|
||||
services.AddHostedService<AobaIndexCreationService>();
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -32,4 +32,5 @@ public class User
|
||||
id.AddClaim(new Claim(ClaimTypes.Role, Role));
|
||||
return id;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,20 +11,20 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" 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>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.9.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.10.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
|
||||
<PackageReference Include="MimeTypesMap" Version="1.0.9" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
</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 System.Net;
|
||||
|
||||
namespace AobaServer.Controllers;
|
||||
|
||||
|
||||
|
||||
//allow login via http during debug testing
|
||||
#if DEBUG
|
||||
[AllowAnonymous]
|
||||
[Route("auth")]
|
||||
public class AuthController : Controller
|
||||
public class AuthController(AccountsService accountsService, AuthInfo authInfo) : Controller
|
||||
{
|
||||
[HttpGet("login")]
|
||||
public IActionResult Login([FromQuery] string returnUrl)
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromForm] string username, [FromForm] string password, CancellationToken cancellationToken)
|
||||
{
|
||||
ViewData["returnUrl"] = returnUrl;
|
||||
return View();
|
||||
}
|
||||
var user = await accountsService.VerifyLoginAsync(username, password, cancellationToken);
|
||||
|
||||
[HttpGet("register/{token}")]
|
||||
public IActionResult Register(string token)
|
||||
if (user == null)
|
||||
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
|
||||
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))
|
||||
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;
|
||||
},
|
||||
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 System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace AobaServer.Utils;
|
||||
|
||||
public static class Extensions
|
||||
@@ -14,5 +23,18 @@ public static class Extensions
|
||||
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