diff --git a/AobaCore/AccountsService.cs b/AobaCore/AccountsService.cs index 3857956..72704e4 100644 --- a/AobaCore/AccountsService.cs +++ b/AobaCore/AccountsService.cs @@ -22,27 +22,28 @@ public class AccountsService(IMongoDatabase db) return await _users.Find(u => u.Id == id).FirstOrDefaultAsync(cancellationToken); } - public async Task VerifyLoginAsync(string username, string password, CancellationToken cancellationToken = default) + public async Task 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.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; diff --git a/AobaCore/AobaCore.csproj b/AobaCore/AobaCore.csproj index e9e25f4..3a18181 100644 --- a/AobaCore/AobaCore.csproj +++ b/AobaCore/AobaCore.csproj @@ -8,11 +8,11 @@ - + - - - + + + diff --git a/AobaCore/AobaService.cs b/AobaCore/AobaService.cs index 4c51792..ffe820f 100644 --- a/AobaCore/AobaService.cs +++ b/AobaCore/AobaService.cs @@ -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) { diff --git a/AobaCore/Extensions.cs b/AobaCore/Extensions.cs index 266c023..a2cd903 100644 --- a/AobaCore/Extensions.cs +++ b/AobaCore/Extensions.cs @@ -23,6 +23,7 @@ public static class Extensions services.AddSingleton(dbClient); services.AddSingleton(db); services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); return services; } diff --git a/AobaCore/Models/User.cs b/AobaCore/Models/User.cs index b06ad75..8b1be15 100644 --- a/AobaCore/Models/User.cs +++ b/AobaCore/Models/User.cs @@ -32,4 +32,5 @@ public class User id.AddClaim(new Claim(ClaimTypes.Role, Role)); return id; } + } diff --git a/AobaServer/AobaServer.csproj b/AobaServer/AobaServer.csproj index 70bff30..46e64b9 100644 --- a/AobaServer/AobaServer.csproj +++ b/AobaServer/AobaServer.csproj @@ -11,20 +11,20 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + - - + + diff --git a/AobaServer/Controllers/Api/MediaApi.cs b/AobaServer/Controllers/Api/MediaApi.cs new file mode 100644 index 0000000..d2d813d --- /dev/null +++ b/AobaServer/Controllers/Api/MediaApi.cs @@ -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 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 Delete(ObjectId id, CancellationToken cancellationToken) + { + await aoba.DeleteFileAsync(id, cancellationToken); + return Ok(); + } +} diff --git a/AobaServer/Controllers/AuthController.cs b/AobaServer/Controllers/AuthController.cs index 6f97b6b..f69414a 100644 --- a/AobaServer/Controllers/AuthController.cs +++ b/AobaServer/Controllers/AuthController.cs @@ -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 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) - { - - return View(token); + if (user == null) + return Problem("Invalid login Credentials", statusCode: StatusCodes.Status400BadRequest); + Response.Cookies.Append("token", user.GetToken(authInfo), new CookieOptions + { + IsEssential = true, + SameSite = SameSiteMode.Strict, + Secure = true, + }); + return Ok(); } } +#endif \ No newline at end of file diff --git a/AobaServer/Models/ShareXDestination.cs b/AobaServer/Models/ShareXDestination.cs new file mode 100644 index 0000000..2be4bfa --- /dev/null +++ b/AobaServer/Models/ShareXDestination.cs @@ -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 Headers { get; set; } = []; + public string Body { get; set; } = "MultipartFormData"; + public Dictionary 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; } +} \ No newline at end of file diff --git a/AobaServer/Program.cs b/AobaServer/Program.cs index 76183c3..2658351 100644 --- a/AobaServer/Program.cs +++ b/AobaServer/Program.cs @@ -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 => diff --git a/AobaServer/Services/AobaAuthService.cs b/AobaServer/Services/AobaAuthService.cs index be73916..fc90c08 100644 --- a/AobaServer/Services/AobaAuthService.cs +++ b/AobaServer/Services/AobaAuthService.cs @@ -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 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 + } + }; + } + } \ No newline at end of file diff --git a/AobaServer/Utils/Extensions.cs b/AobaServer/Utils/Extensions.cs index 324bf39..9ef2502 100644 --- a/AobaServer/Utils/Extensions.cs +++ b/AobaServer/Utils/Extensions.cs @@ -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(); + } }