diff --git a/Moonlight/App/Database/Entities/SupportChatSnippets.cs b/Moonlight/App/Database/Entities/SupportChatSnippets.cs new file mode 100644 index 00000000..b4021ae2 --- /dev/null +++ b/Moonlight/App/Database/Entities/SupportChatSnippets.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Database.Entities; + +public class SupportChatSnippets +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs index 254a0b5a..24dbdc26 100644 --- a/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Moonlight.App.Helpers; using Moonlight.App.Models.Misc; +using Moonlight.App.Services; using Moonlight.App.Services.LogServices; namespace Moonlight.App.Http.Controllers.Api.Moonlight; @@ -11,10 +12,13 @@ namespace Moonlight.App.Http.Controllers.Api.Moonlight; public class ResourcesController : Controller { private readonly SecurityLogService SecurityLogService; + private readonly BucketService BucketService; - public ResourcesController(SecurityLogService securityLogService) + public ResourcesController(SecurityLogService securityLogService, + BucketService bucketService) { SecurityLogService = securityLogService; + BucketService = bucketService; } [HttpGet("images/{name}")] @@ -26,6 +30,7 @@ public class ResourcesController : Controller { x.Add(name); }); + return NotFound(); } @@ -38,4 +43,33 @@ public class ResourcesController : Controller return NotFound(); } + + [HttpGet("bucket/{bucket}/{name}")] + public async Task GetBucket([FromRoute] string bucket, [FromRoute] string name) + { + if (name.Contains("..")) + { + await SecurityLogService.Log(SecurityLogType.PathTransversal, x => + { + x.Add(name); + }); + + return NotFound(); + } + + try + { + var fs = await BucketService.GetFile(bucket, name); + + return File(fs, MimeTypes.GetMimeType(name), name); + } + catch (FileNotFoundException) + { + return NotFound(); + } + catch (Exception) + { + return Problem(); + } + } } \ No newline at end of file diff --git a/Moonlight/App/Services/BucketService.cs b/Moonlight/App/Services/BucketService.cs new file mode 100644 index 00000000..a1417dfa --- /dev/null +++ b/Moonlight/App/Services/BucketService.cs @@ -0,0 +1,68 @@ +using Logging.Net; +using Moonlight.App.Helpers; + +namespace Moonlight.App.Services; + +public class BucketService +{ + private string BucketPath; + + public BucketService() + { + BucketPath = PathBuilder.Dir("storage", "uploads"); + } + + public Task GetBuckets() + { + var buckets = Directory.GetDirectories(BucketPath) + .Select(x => + x.Replace(BucketPath, "").TrimEnd('/') + ) + .ToArray(); + + return Task.FromResult(buckets); + } + + private Task EnsureBucket(string name) + { + Directory.CreateDirectory(BucketPath + name); + + return Task.CompletedTask; + } + + public async Task StoreFile(string bucket, Stream dataStream, string? name = null) + { + await EnsureBucket(bucket); + + var extension = ""; + + if (name != null) + extension = Path.GetExtension(name); + + var fileName = Path.GetRandomFileName() + extension; //TODO: Add check for existing file + var filePath = BucketPath + PathBuilder.File(bucket, fileName); + + var fileStream = File.Create(filePath); + + await dataStream.CopyToAsync(fileStream); + await fileStream.FlushAsync(); + + fileStream.Close(); + + return fileName; + } + + public Task GetFile(string bucket, string file) + { + var filePath = BucketPath + PathBuilder.File(bucket, file); + + if (File.Exists(filePath)) + { + var stream = File.Open(filePath, FileMode.Open); + + return Task.FromResult(stream); + } + else + throw new FileNotFoundException(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/ResourceService.cs b/Moonlight/App/Services/ResourceService.cs index 02254213..8b126340 100644 --- a/Moonlight/App/Services/ResourceService.cs +++ b/Moonlight/App/Services/ResourceService.cs @@ -20,4 +20,9 @@ public class ResourceService { return $"{AppUrl}/api/moonlight/avatar/{user.Id}"; } + + public string BucketItem(string bucket, string name) + { + return $"{AppUrl}/api/moonlight/resources/bucket/{bucket}/{name}"; + } } \ No newline at end of file diff --git a/Moonlight/App/Services/SupportChat/SupportChatAdminService.cs b/Moonlight/App/Services/SupportChat/SupportChatAdminService.cs index 6369f34a..d77b355e 100644 --- a/Moonlight/App/Services/SupportChat/SupportChatAdminService.cs +++ b/Moonlight/App/Services/SupportChat/SupportChatAdminService.cs @@ -1,14 +1,16 @@ -using Moonlight.App.Database.Entities; +using Microsoft.AspNetCore.Components.Forms; +using Moonlight.App.Database.Entities; using Moonlight.App.Events; using Moonlight.App.Services.Sessions; namespace Moonlight.App.Services.SupportChat; -public class SupportChatAdminService +public class SupportChatAdminService : IDisposable { private readonly EventSystem Event; private readonly IdentityService IdentityService; private readonly SupportChatServerService ServerService; + private readonly BucketService BucketService; public Func? OnMessage { get; set; } public Func? OnTypingChanged { get; set; } @@ -20,11 +22,13 @@ public class SupportChatAdminService public SupportChatAdminService( EventSystem eventSystem, SupportChatServerService serverService, - IdentityService identityService) + IdentityService identityService, + BucketService bucketService) { Event = eventSystem; ServerService = serverService; IdentityService = identityService; + BucketService = bucketService; } public async Task Start(User recipient) @@ -60,11 +64,21 @@ public class SupportChatAdminService return await ServerService.GetMessages(Recipient); } - public async Task SendMessage(string content) + public async Task SendMessage(string content, IBrowserFile? browserFile = null) { if (User != null) { - return await ServerService.SendMessage(Recipient, content, User); + string? attachment = null; + + if (browserFile != null) + { + attachment = await BucketService.StoreFile( + "supportChat", + browserFile.OpenReadStream(1024 * 1024 * 5), + browserFile.Name); + } + + return await ServerService.SendMessage(Recipient, content, User, attachment); } return null!; diff --git a/Moonlight/App/Services/SupportChat/SupportChatClientService.cs b/Moonlight/App/Services/SupportChat/SupportChatClientService.cs index ad2b3eb7..c6a10f9c 100644 --- a/Moonlight/App/Services/SupportChat/SupportChatClientService.cs +++ b/Moonlight/App/Services/SupportChat/SupportChatClientService.cs @@ -1,4 +1,5 @@ using Logging.Net; +using Microsoft.AspNetCore.Components.Forms; using Moonlight.App.Database.Entities; using Moonlight.App.Events; using Moonlight.App.Services.Sessions; @@ -8,9 +9,10 @@ namespace Moonlight.App.Services.SupportChat; public class SupportChatClientService : IDisposable { private readonly EventSystem Event; + private readonly BucketService BucketService; private readonly IdentityService IdentityService; private readonly SupportChatServerService ServerService; - + public Func? OnMessage { get; set; } public Func? OnTypingChanged { get; set; } @@ -20,11 +22,13 @@ public class SupportChatClientService : IDisposable public SupportChatClientService( EventSystem eventSystem, SupportChatServerService serverService, - IdentityService identityService) + IdentityService identityService, + BucketService bucketService) { Event = eventSystem; ServerService = serverService; IdentityService = identityService; + BucketService = bucketService; } public async Task Start() @@ -59,11 +63,21 @@ public class SupportChatClientService : IDisposable return await ServerService.GetMessages(User); } - public async Task SendMessage(string content) + public async Task SendMessage(string content, IBrowserFile? browserFile = null) { if (User != null) { - return await ServerService.SendMessage(User, content, User); + string? attachment = null; + + if (browserFile != null) + { + attachment = await BucketService.StoreFile( + "supportChat", + browserFile.OpenReadStream(1024 * 1024 * 5), + browserFile.Name); + } + + return await ServerService.SendMessage(User, content, User, attachment); } return null!; diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 7621c7bf..725fde34 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -108,6 +108,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Moonlight/Shared/Components/Forms/SmartFileSelect.razor b/Moonlight/Shared/Components/Forms/SmartFileSelect.razor new file mode 100644 index 00000000..ed8b7d73 --- /dev/null +++ b/Moonlight/Shared/Components/Forms/SmartFileSelect.razor @@ -0,0 +1,61 @@ +@using Moonlight.App.Helpers.Files +@using Moonlight.App.Services +@using Moonlight.App.Services.Interop +@using Logging.Net + +@inject ToastService ToastService +@inject SmartTranslateService SmartTranslateService + +