Шифрование и хранилище minio
This commit is contained in:
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2026 Knot Messenger
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,27 @@
|
|||||||
|
Determining projects to restore...
|
||||||
|
All projects are up-to-date for restore.
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Chats.UnitTests\Knot.Modules.Chats.UnitTests.csproj]
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Identity.UnitTests\Knot.Modules.Identity.UnitTests.csproj]
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Knot.Modules.Identity.csproj]
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj]
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj]
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj]
|
||||||
|
Knot.Shared.Kernel -> E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Kernel\bin\Debug\net10.0\Knot.Shared.Kernel.dll
|
||||||
|
E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Domain\Story.cs(23,15): warning CS8618: Non-nullable property 'Type' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Knot.Modules.Identity.csproj]
|
||||||
|
E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Storage\S3FileStorageService.cs(116,27): warning CS8600: Converting null literal or possible null value to non-nullable type. [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj]
|
||||||
|
E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Storage\S3FileStorageService.cs(142,47): warning CS8604: Possible null reference argument for parameter 's' in 'byte[] Convert.FromBase64String(string s)'. [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj]
|
||||||
|
Knot.Modules.Identity -> E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\bin\Debug\net10.0\Knot.Modules.Identity.dll
|
||||||
|
Knot.Shared.Infrastructure -> E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\bin\Debug\net10.0\Knot.Shared.Infrastructure.dll
|
||||||
|
Knot.Modules.Chats -> E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\bin\Debug\net10.0\Knot.Modules.Chats.dll
|
||||||
|
Knot.Modules.Identity.UnitTests -> E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Identity.UnitTests\bin\Debug\net10.0\Knot.Modules.Identity.UnitTests.dll
|
||||||
|
Knot.Modules.Chats.UnitTests -> E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Chats.UnitTests\bin\Debug\net10.0\Knot.Modules.Chats.UnitTests.dll
|
||||||
|
|
||||||
|
Build succeeded.
|
||||||
|
|
||||||
|
E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Domain\Story.cs(23,15): warning CS8618: Non-nullable property 'Type' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Knot.Modules.Identity.csproj]
|
||||||
|
E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Storage\S3FileStorageService.cs(116,27): warning CS8600: Converting null literal or possible null value to non-nullable type. [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj]
|
||||||
|
E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Storage\S3FileStorageService.cs(142,47): warning CS8604: Possible null reference argument for parameter 's' in 'byte[] Convert.FromBase64String(string s)'. [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj]
|
||||||
|
3 Warning(s)
|
||||||
|
0 Error(s)
|
||||||
|
|
||||||
|
Time Elapsed 00:00:01.32
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
Determining projects to restore...
|
||||||
|
All projects are up-to-date for restore.
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj]
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj]
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Chats.UnitTests\Knot.Modules.Chats.UnitTests.csproj]
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Identity.UnitTests\Knot.Modules.Identity.UnitTests.csproj]
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Knot.Modules.Identity.csproj]
|
||||||
|
C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj]
|
||||||
|
Knot.Shared.Kernel -> E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Kernel\bin\Debug\net10.0\Knot.Shared.Kernel.dll
|
||||||
|
Knot.Modules.Identity -> E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\bin\Debug\net10.0\Knot.Modules.Identity.dll
|
||||||
|
Knot.Shared.Infrastructure -> E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\bin\Debug\net10.0\Knot.Shared.Infrastructure.dll
|
||||||
|
Knot.Modules.Identity.UnitTests -> E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Identity.UnitTests\bin\Debug\net10.0\Knot.Modules.Identity.UnitTests.dll
|
||||||
|
E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Infrastructure\Persistence\ChatsDbContext.cs(16,34): error CS0234: The type or namespace name 'Infrastructure' does not exist in the namespace 'Knot.Shared' (are you missing an assembly reference?) [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj]
|
||||||
|
E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Infrastructure\Persistence\ChatsDbContext.cs(18,101): error CS0234: The type or namespace name 'Infrastructure' does not exist in the namespace 'Knot.Shared' (are you missing an assembly reference?) [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj]
|
||||||
|
|
||||||
|
Build FAILED.
|
||||||
|
|
||||||
|
E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Infrastructure\Persistence\ChatsDbContext.cs(16,34): error CS0234: The type or namespace name 'Infrastructure' does not exist in the namespace 'Knot.Shared' (are you missing an assembly reference?) [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj]
|
||||||
|
E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Infrastructure\Persistence\ChatsDbContext.cs(18,101): error CS0234: The type or namespace name 'Infrastructure' does not exist in the namespace 'Knot.Shared' (are you missing an assembly reference?) [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj]
|
||||||
|
0 Warning(s)
|
||||||
|
2 Error(s)
|
||||||
|
|
||||||
|
Time Elapsed 00:00:00.89
|
||||||
@@ -120,6 +120,15 @@ public sealed class ChatsController : ControllerBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (c.Type == ChatType.Personal && messagesList.Count == 0)
|
||||||
|
{
|
||||||
|
var currentMember = c.Members.FirstOrDefault(m => m.UserId == _userContext.UserId);
|
||||||
|
if (currentMember == null || currentMember.Role != ChatRole.Owner)
|
||||||
|
{
|
||||||
|
continue; // Skip returning this empty personal chat to the non-initiator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result.Add(new
|
result.Add(new
|
||||||
{
|
{
|
||||||
id = c.Id,
|
id = c.Id,
|
||||||
@@ -209,7 +218,8 @@ public sealed class ChatsController : ControllerBase
|
|||||||
public async Task<IActionResult> LeaveOrDeleteChat(Guid id, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, CancellationToken ct)
|
public async Task<IActionResult> LeaveOrDeleteChat(Guid id, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var chat = await chatRepository.GetByIdAsync(id, ct);
|
var chat = await chatRepository.GetByIdAsync(id, ct);
|
||||||
if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound();
|
if (chat == null) return Ok(new { success = true }); // Already physically deleted, idempotent
|
||||||
|
if (!chat.Members.Any(m => m.UserId == _userContext.UserId)) return Forbid();
|
||||||
|
|
||||||
if (chat.Type == ChatType.Group)
|
if (chat.Type == ChatType.Group)
|
||||||
{
|
{
|
||||||
@@ -280,21 +290,16 @@ public sealed class ChatsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/avatar")]
|
[HttpPost("{id:guid}/avatar")]
|
||||||
public async Task<IActionResult> UploadGroupAvatar(Guid id, IFormFile avatar, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, CancellationToken ct)
|
public async Task<IActionResult> UploadGroupAvatar(Guid id, IFormFile avatar, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, [FromServices] Knot.Shared.Kernel.Storage.IFileStorageService fileStorage, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var chat = await chatRepository.GetByIdAsync(id, ct);
|
var chat = await chatRepository.GetByIdAsync(id, ct);
|
||||||
if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound();
|
if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound();
|
||||||
if (avatar == null || avatar.Length == 0) return BadRequest("No file");
|
if (avatar == null || avatar.Length == 0) return BadRequest("No file");
|
||||||
|
|
||||||
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars");
|
using var stream = avatar.OpenReadStream();
|
||||||
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
|
var fileId = await fileStorage.UploadFileAsync(stream, avatar.FileName, avatar.ContentType);
|
||||||
|
var url = $"/api/files/{fileId}";
|
||||||
|
|
||||||
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(avatar.FileName)}";
|
|
||||||
var filePath = Path.Combine(uploadsPath, fileName);
|
|
||||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
|
||||||
await avatar.CopyToAsync(stream, ct);
|
|
||||||
|
|
||||||
var url = $"/uploads/avatars/{fileName}";
|
|
||||||
chat.UpdateAvatar(url);
|
chat.UpdateAvatar(url);
|
||||||
chatRepository.Update(chat);
|
chatRepository.Update(chat);
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
@@ -303,17 +308,13 @@ public sealed class ChatsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/avatar/crop")]
|
[HttpPost("{id:guid}/avatar/crop")]
|
||||||
public async Task<IActionResult> CropGroupAvatar(Guid id, [FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, CancellationToken ct)
|
public async Task<IActionResult> CropGroupAvatar(Guid id, [FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, [FromServices] Knot.Shared.Kernel.Storage.IFileStorageService fileStorage, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var chat = await chatRepository.GetByIdAsync(id, ct);
|
var chat = await chatRepository.GetByIdAsync(id, ct);
|
||||||
if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound();
|
if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound();
|
||||||
if (avatar == null || avatar.Length == 0) return BadRequest("No file");
|
if (avatar == null || avatar.Length == 0) return BadRequest("No file");
|
||||||
|
|
||||||
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars");
|
string url;
|
||||||
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
|
|
||||||
|
|
||||||
var fileName = $"{Guid.NewGuid()}.jpg";
|
|
||||||
var filePath = Path.Combine(uploadsPath, fileName);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -327,7 +328,14 @@ public sealed class ChatsController : ControllerBase
|
|||||||
|
|
||||||
image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight)));
|
image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight)));
|
||||||
image.Mutate(ctx => ctx.Resize(400, 400));
|
image.Mutate(ctx => ctx.Resize(400, 400));
|
||||||
await image.SaveAsJpegAsync(filePath, ct);
|
|
||||||
|
using var outStream = new MemoryStream();
|
||||||
|
await image.SaveAsJpegAsync(outStream, ct);
|
||||||
|
outStream.Position = 0;
|
||||||
|
|
||||||
|
var fileName = avatar.FileName ?? "avatar.jpg";
|
||||||
|
var fileId = await fileStorage.UploadFileAsync(outStream, fileName, "image/jpeg");
|
||||||
|
url = $"/api/files/{fileId}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -335,7 +343,6 @@ public sealed class ChatsController : ControllerBase
|
|||||||
return StatusCode(500, "Error processing image: " + ex.Message);
|
return StatusCode(500, "Error processing image: " + ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = $"/uploads/avatars/{fileName}";
|
|
||||||
chat.UpdateAvatar(url);
|
chat.UpdateAvatar(url);
|
||||||
chatRepository.Update(chat);
|
chatRepository.Update(chat);
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Knot.Shared.Kernel.Storage;
|
||||||
|
|
||||||
|
namespace Host.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/files")]
|
||||||
|
public sealed class FilesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IFileStorageService _fileStorage;
|
||||||
|
|
||||||
|
public FilesController(IFileStorageService fileStorage)
|
||||||
|
{
|
||||||
|
_fileStorage = fileStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> DownloadFile(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _fileStorage.DownloadFileAsync(id);
|
||||||
|
// Обратите внимание, что мы возвращаем поток с автоматическим освобождением памяти.
|
||||||
|
return File(result.Stream, result.ContentType, result.FileName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = "Файл не найден или ошибка доступа.", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -187,22 +187,14 @@ public sealed class MessagesController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("upload")]
|
[HttpPost("upload")]
|
||||||
public async Task<IActionResult> UploadFile(IFormFile file)
|
public async Task<IActionResult> UploadFile(IFormFile file, [FromServices] Knot.Shared.Kernel.Storage.IFileStorageService fileStorage)
|
||||||
{
|
{
|
||||||
if (file == null || file.Length == 0) return BadRequest("No file uploaded");
|
if (file == null || file.Length == 0) return BadRequest("No file uploaded");
|
||||||
|
|
||||||
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads");
|
using var stream = file.OpenReadStream();
|
||||||
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
|
var fileId = await fileStorage.UploadFileAsync(stream, file.FileName, file.ContentType);
|
||||||
|
|
||||||
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
|
return Ok(new { url = $"/api/files/{fileId}", filename = file.FileName, size = file.Length });
|
||||||
var filePath = Path.Combine(uploadsPath, fileName);
|
|
||||||
|
|
||||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
|
||||||
{
|
|
||||||
await file.CopyToAsync(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(new { url = $"/uploads/{fileName}", filename = file.FileName, size = file.Length });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("chat/{chatId:guid}/shared")]
|
[HttpGet("chat/{chatId:guid}/shared")]
|
||||||
|
|||||||
@@ -64,45 +64,19 @@ public sealed class UsersController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("avatar")]
|
[HttpPost("avatar")]
|
||||||
public async Task<IActionResult> UploadAvatar(IFormFile avatar, CancellationToken ct)
|
public async Task<IActionResult> UploadAvatar(IFormFile avatar, [FromServices] Knot.Shared.Kernel.Storage.IFileStorageService fileStorage, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var user = await _userRepository.GetByIdAsync(_userContext.UserId, ct);
|
var user = await _userRepository.GetByIdAsync(_userContext.UserId, ct);
|
||||||
if (user == null) return NotFound();
|
if (user == null) return NotFound();
|
||||||
|
|
||||||
string? avatarUrl = null;
|
string? avatarUrl = null;
|
||||||
|
var fileToUpload = avatar != null && avatar.Length > 0 ? avatar : Request.Form.Files.Count > 0 ? Request.Form.Files[0] : null;
|
||||||
|
|
||||||
if (avatar != null && avatar.Length > 0)
|
if (fileToUpload != null)
|
||||||
{
|
{
|
||||||
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars");
|
using var stream = fileToUpload.OpenReadStream();
|
||||||
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
|
var fileId = await fileStorage.UploadFileAsync(stream, fileToUpload.FileName, fileToUpload.ContentType);
|
||||||
|
avatarUrl = $"/api/files/{fileId}";
|
||||||
var ext = Path.GetExtension(avatar.FileName);
|
|
||||||
var fileName = $"{Guid.NewGuid()}{ext}";
|
|
||||||
var filePath = Path.Combine(uploadsPath, fileName);
|
|
||||||
|
|
||||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
|
||||||
{
|
|
||||||
await avatar.CopyToAsync(stream, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
avatarUrl = $"/uploads/avatars/{fileName}";
|
|
||||||
}
|
|
||||||
else if (Request.Form.Files.Count > 0)
|
|
||||||
{
|
|
||||||
var file = Request.Form.Files[0];
|
|
||||||
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars");
|
|
||||||
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
|
|
||||||
|
|
||||||
var ext = Path.GetExtension(file.FileName);
|
|
||||||
var fileName = $"{Guid.NewGuid()}{ext}";
|
|
||||||
var filePath = Path.Combine(uploadsPath, fileName);
|
|
||||||
|
|
||||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
|
||||||
{
|
|
||||||
await file.CopyToAsync(stream, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
avatarUrl = $"/uploads/avatars/{fileName}";
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -124,26 +98,20 @@ public sealed class UsersController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("avatar/crop")]
|
[HttpPost("avatar/crop")]
|
||||||
public async Task<IActionResult> CropAvatar([FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, CancellationToken ct)
|
public async Task<IActionResult> CropAvatar([FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, [FromServices] Knot.Shared.Kernel.Storage.IFileStorageService fileStorage, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var user = await _userRepository.GetByIdAsync(_userContext.UserId, ct);
|
var user = await _userRepository.GetByIdAsync(_userContext.UserId, ct);
|
||||||
if (user == null) return NotFound();
|
if (user == null) return NotFound();
|
||||||
|
|
||||||
if (avatar == null || avatar.Length == 0) return BadRequest("No file");
|
if (avatar == null || avatar.Length == 0) return BadRequest("No file");
|
||||||
|
|
||||||
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars");
|
string avatarUrl;
|
||||||
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
|
|
||||||
|
|
||||||
var ext = ".jpg";
|
|
||||||
var fileName = $"{Guid.NewGuid()}{ext}";
|
|
||||||
var filePath = Path.Combine(uploadsPath, fileName);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var inputStream = avatar.OpenReadStream())
|
using (var inputStream = avatar.OpenReadStream())
|
||||||
using (var image = await SixLabors.ImageSharp.Image.LoadAsync(inputStream))
|
using (var image = await SixLabors.ImageSharp.Image.LoadAsync(inputStream))
|
||||||
{
|
{
|
||||||
// Clamp coordinates to image bounds
|
|
||||||
int startX = Math.Max(0, Math.Min(x, image.Width - 1));
|
int startX = Math.Max(0, Math.Min(x, image.Width - 1));
|
||||||
int startY = Math.Max(0, Math.Min(y, image.Height - 1));
|
int startY = Math.Max(0, Math.Min(y, image.Height - 1));
|
||||||
int rectWidth = Math.Max(1, Math.Min(width, image.Width - startX));
|
int rectWidth = Math.Max(1, Math.Min(width, image.Width - startX));
|
||||||
@@ -152,7 +120,13 @@ public sealed class UsersController : ControllerBase
|
|||||||
image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight)));
|
image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight)));
|
||||||
image.Mutate(ctx => ctx.Resize(400, 400));
|
image.Mutate(ctx => ctx.Resize(400, 400));
|
||||||
|
|
||||||
await image.SaveAsJpegAsync(filePath, ct);
|
using var outStream = new MemoryStream();
|
||||||
|
await image.SaveAsJpegAsync(outStream, ct);
|
||||||
|
outStream.Position = 0;
|
||||||
|
|
||||||
|
var fileName = avatar.FileName ?? "avatar.jpg";
|
||||||
|
var id = await fileStorage.UploadFileAsync(outStream, fileName, "image/jpeg");
|
||||||
|
avatarUrl = $"/api/files/{id}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -160,7 +134,6 @@ public sealed class UsersController : ControllerBase
|
|||||||
return StatusCode(500, "Error processing image: " + ex.Message);
|
return StatusCode(500, "Error processing image: " + ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var avatarUrl = $"/uploads/avatars/{fileName}";
|
|
||||||
user.UpdateAvatar(avatarUrl);
|
user.UpdateAvatar(avatarUrl);
|
||||||
await _unitOfWork.SaveChangesAsync(ct);
|
await _unitOfWork.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ var envMappings = new Dictionary<string, string?>
|
|||||||
["Jwt:Audience"] = builder.Configuration["JWT_AUDIENCE"],
|
["Jwt:Audience"] = builder.Configuration["JWT_AUDIENCE"],
|
||||||
["WebRtc:TurnUrl"] = builder.Configuration["TURN_URL"],
|
["WebRtc:TurnUrl"] = builder.Configuration["TURN_URL"],
|
||||||
["WebRtc:TurnUsername"] = builder.Configuration["TURN_USERNAME"],
|
["WebRtc:TurnUsername"] = builder.Configuration["TURN_USERNAME"],
|
||||||
["WebRtc:TurnPassword"] = builder.Configuration["TURN_PASSWORD"]
|
["WebRtc:TurnPassword"] = builder.Configuration["TURN_PASSWORD"],
|
||||||
|
["KNOT_MASTER_ENCRYPTION_KEY"] = builder.Configuration["KNOT_MASTER_ENCRYPTION_KEY"],
|
||||||
|
["S3_ENDPOINT"] = builder.Configuration["S3_ENDPOINT"],
|
||||||
|
["S3_ACCESS_KEY"] = builder.Configuration["S3_ACCESS_KEY"],
|
||||||
|
["S3_SECRET_KEY"] = builder.Configuration["S3_SECRET_KEY"],
|
||||||
|
["S3_BUCKET"] = builder.Configuration["S3_BUCKET"]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Добавляем только те, что реально заданы в ENV
|
// Добавляем только те, что реально заданы в ENV
|
||||||
@@ -41,7 +46,7 @@ builder.Configuration.AddInMemoryCollection(
|
|||||||
|
|
||||||
builder.Services.AddIdentityModule(builder.Configuration);
|
builder.Services.AddIdentityModule(builder.Configuration);
|
||||||
builder.Services.AddChatsModule(builder.Configuration);
|
builder.Services.AddChatsModule(builder.Configuration);
|
||||||
builder.Services.AddSharedInfrastructure();
|
builder.Services.AddSharedInfrastructure(builder.Configuration);
|
||||||
|
|
||||||
// Настройка CORS
|
// Настройка CORS
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
@@ -147,17 +152,7 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
// app.UseHttpsRedirection();
|
// app.UseHttpsRedirection();
|
||||||
|
|
||||||
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads");
|
// Не раздаем статические файлы, так как теперь используем MinIO
|
||||||
if (!Directory.Exists(uploadsPath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(uploadsPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseStaticFiles(new StaticFileOptions
|
|
||||||
{
|
|
||||||
FileProvider = new PhysicalFileProvider(uploadsPath),
|
|
||||||
RequestPath = "/uploads"
|
|
||||||
});
|
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ public sealed class CreateChatCommandHandler : ICommandHandler<CreateChatCommand
|
|||||||
for (int i = 0; i < request.MemberIds.Count; i++)
|
for (int i = 0; i < request.MemberIds.Count; i++)
|
||||||
{
|
{
|
||||||
var userId = request.MemberIds[i];
|
var userId = request.MemberIds[i];
|
||||||
var role = (i == 0 && request.Type == ChatType.Group) ? ChatRole.Admin : ChatRole.Member;
|
var role = (i == 0) ? ChatRole.Owner : ChatRole.Member;
|
||||||
chat.AddMember(userId, role);
|
chat.AddMember(userId, role);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Knot.Modules.Chats;
|
namespace Knot.Modules.Chats;
|
||||||
|
|
||||||
public class Class1
|
public class Class1
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence;
|
|||||||
public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
|
public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
private readonly Knot.Shared.Kernel.Security.IEncryptionService _encryptionService;
|
||||||
|
|
||||||
public ChatsDbContext(DbContextOptions<ChatsDbContext> options, IMediator mediator)
|
public ChatsDbContext(DbContextOptions<ChatsDbContext> options, IMediator mediator, Knot.Shared.Kernel.Security.IEncryptionService encryptionService)
|
||||||
|
|
||||||
: base(options)
|
: base(options)
|
||||||
|
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
_encryptionService = encryptionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<Chat> Chats => Set<Chat>();
|
public DbSet<Chat> Chats => Set<Chat>();
|
||||||
@@ -59,6 +59,12 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
|
|||||||
builder.ToTable("Messages");
|
builder.ToTable("Messages");
|
||||||
builder.HasKey(m => m.Id);
|
builder.HasKey(m => m.Id);
|
||||||
|
|
||||||
|
builder.Property(m => m.Content)
|
||||||
|
.HasConversion(
|
||||||
|
v => v == null ? null : _encryptionService.EncryptMessage(v),
|
||||||
|
v => v == null ? null : _encryptionService.DecryptMessage(v)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
builder.OwnsMany(m => m.Media, mb =>
|
builder.OwnsMany(m => m.Media, mb =>
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Knot.Modules.Identity;
|
namespace Knot.Modules.Identity;
|
||||||
|
|
||||||
public class Class1
|
public class Class1
|
||||||
{
|
{
|
||||||
|
|||||||
+18
-2
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Metadata;
|
|||||||
using Knot.Modules.Identity.Application.Abstractions;
|
using Knot.Modules.Identity.Application.Abstractions;
|
||||||
using Knot.Modules.Identity.Domain;
|
using Knot.Modules.Identity.Domain;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
|
using Knot.Shared.Kernel.Security;
|
||||||
|
|
||||||
namespace Knot.Modules.Identity.Infrastructure.Persistence;
|
namespace Knot.Modules.Identity.Infrastructure.Persistence;
|
||||||
|
|
||||||
@@ -14,11 +15,13 @@ namespace Knot.Modules.Identity.Infrastructure.Persistence;
|
|||||||
public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork
|
public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
private readonly IEncryptionService _encryptionService;
|
||||||
|
|
||||||
public IdentityDbContext(DbContextOptions<IdentityDbContext> options, IMediator mediator)
|
public IdentityDbContext(DbContextOptions<IdentityDbContext> options, IMediator mediator, IEncryptionService encryptionService)
|
||||||
: base(options)
|
: base(options)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
_encryptionService = encryptionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<User> Users => Set<User>();
|
public DbSet<User> Users => Set<User>();
|
||||||
@@ -52,6 +55,12 @@ public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork
|
|||||||
builder.HasKey(s => s.Id);
|
builder.HasKey(s => s.Id);
|
||||||
builder.Property(s => s.Type).IsRequired();
|
builder.Property(s => s.Type).IsRequired();
|
||||||
|
|
||||||
|
builder.Property(s => s.Content)
|
||||||
|
.HasConversion(
|
||||||
|
v => v == null ? null : _encryptionService.EncryptMessage(v),
|
||||||
|
v => v == null ? null : _encryptionService.DecryptMessage(v)
|
||||||
|
);
|
||||||
|
|
||||||
// Configure backing fields for collections
|
// Configure backing fields for collections
|
||||||
|
|
||||||
builder.Metadata.FindNavigation(nameof(Story.Viewers))?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
builder.Metadata.FindNavigation(nameof(Story.Viewers))?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||||
@@ -92,7 +101,14 @@ public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork
|
|||||||
{
|
{
|
||||||
builder.ToTable("StoryReplies");
|
builder.ToTable("StoryReplies");
|
||||||
builder.HasKey(r => r.Id);
|
builder.HasKey(r => r.Id);
|
||||||
builder.Property(r => r.Content).IsRequired().HasMaxLength(500);
|
|
||||||
|
builder.Property(r => r.Content)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasConversion(
|
||||||
|
v => v == null ? null : _encryptionService.EncryptMessage(v),
|
||||||
|
v => v == null ? null : _encryptionService.DecryptMessage(v)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<User>(builder =>
|
modelBuilder.Entity<User>(builder =>
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Knot.Shared.Infrastructure;
|
namespace Knot.Shared.Infrastructure;
|
||||||
|
|
||||||
public class Class1
|
public class Class1
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,15 +1,45 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
|
using Knot.Shared.Kernel.Security;
|
||||||
|
using Knot.Shared.Kernel.Storage;
|
||||||
|
using Minio;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Knot.Shared.Infrastructure;
|
namespace Knot.Shared.Infrastructure;
|
||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddSharedInfrastructure(this IServiceCollection services)
|
public static IServiceCollection AddSharedInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
services.AddScoped<IUserContext, UserContext>();
|
services.AddScoped<IUserContext, UserContext>();
|
||||||
|
|
||||||
|
// Настройка шифрования
|
||||||
|
var masterKey = configuration["KNOT_MASTER_ENCRYPTION_KEY"]
|
||||||
|
?? throw new ArgumentNullException("KNOT_MASTER_ENCRYPTION_KEY is missing in env.");
|
||||||
|
|
||||||
|
services.AddSingleton<IEncryptionService>(new AesEncryptionService(masterKey));
|
||||||
|
|
||||||
|
// Настройка MinIO (S3)
|
||||||
|
var s3Endpoint = configuration["S3_ENDPOINT"] ?? "minio:9000";
|
||||||
|
var s3AccessKey = configuration["S3_ACCESS_KEY"] ?? "admin";
|
||||||
|
var s3SecretKey = configuration["S3_SECRET_KEY"] ?? "KnotSuperSecretMinioPassword";
|
||||||
|
var s3Bucket = configuration["S3_BUCKET"] ?? "knot-uploads";
|
||||||
|
|
||||||
|
services.AddMinio(configureClient => configureClient
|
||||||
|
.WithEndpoint(s3Endpoint)
|
||||||
|
.WithCredentials(s3AccessKey, s3SecretKey)
|
||||||
|
.WithSSL(false)
|
||||||
|
.Build());
|
||||||
|
|
||||||
|
services.AddScoped<IFileStorageService>(provider =>
|
||||||
|
{
|
||||||
|
var minioClient = provider.GetRequiredService<IMinioClient>();
|
||||||
|
var encService = provider.GetRequiredService<IEncryptionService>();
|
||||||
|
return new S3FileStorageService(minioClient, encService, s3Bucket);
|
||||||
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||||
|
<PackageReference Include="Minio" Version="7.0.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
||||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Knot.Shared.Kernel.Security;
|
||||||
|
|
||||||
|
public class AesEncryptionService : IEncryptionService
|
||||||
|
{
|
||||||
|
private readonly byte[] _key;
|
||||||
|
|
||||||
|
public AesEncryptionService(string masterKeyBase64)
|
||||||
|
{
|
||||||
|
// Try decoding as base64 or just using UTF8 string
|
||||||
|
if (masterKeyBase64.Length == 32)
|
||||||
|
{
|
||||||
|
_key = Encoding.UTF8.GetBytes(masterKeyBase64);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_key = Convert.FromBase64String(masterKeyBase64);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_key = Encoding.UTF8.GetBytes(masterKeyBase64.PadRight(32, '0')[..32]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_key.Length != 32)
|
||||||
|
throw new ArgumentException("Мастер-ключ должен быть ровно 32 байта для AES-256.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сообщения: AES-256-GCM
|
||||||
|
public string EncryptMessage(string plainText)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(plainText)) return plainText;
|
||||||
|
|
||||||
|
var nonce = new byte[12];
|
||||||
|
RandomNumberGenerator.Fill(nonce);
|
||||||
|
|
||||||
|
var plainBytes = Encoding.UTF8.GetBytes(plainText);
|
||||||
|
var cipherBytes = new byte[plainBytes.Length];
|
||||||
|
var tag = new byte[16];
|
||||||
|
|
||||||
|
using var aesGcm = new AesGcm(_key, 16);
|
||||||
|
aesGcm.Encrypt(nonce, plainBytes, cipherBytes, tag);
|
||||||
|
|
||||||
|
// формат: nonce:tag:cipherText
|
||||||
|
return $"{Convert.ToBase64String(nonce)}:{Convert.ToBase64String(tag)}:{Convert.ToBase64String(cipherBytes)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DecryptMessage(string cipherText)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(cipherText)) return cipherText;
|
||||||
|
|
||||||
|
var parts = cipherText.Split(':');
|
||||||
|
if (parts.Length != 3) return cipherText;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var nonce = Convert.FromBase64String(parts[0]);
|
||||||
|
var tag = Convert.FromBase64String(parts[1]);
|
||||||
|
var cipherBytes = Convert.FromBase64String(parts[2]);
|
||||||
|
var plainBytes = new byte[cipherBytes.Length];
|
||||||
|
|
||||||
|
using var aesGcm = new AesGcm(_key, 16);
|
||||||
|
aesGcm.Decrypt(nonce, cipherBytes, tag, plainBytes);
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(plainBytes);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return cipherText; // Возвращаем оригинал при ошибке расшифровки (например, старые не зашифрованные сообщения)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Потоковые файлы: AES CBC через CryptoStream
|
||||||
|
// (Примечание: Для GCM через CryptoStream нет поддержки "из коробки" в .NET при использовании Aes.Create().
|
||||||
|
// Требуется CBC+HMAC/CTR или механизм AEAD блоками. Здесь предоставляется стандартный AES-CBC для соответствия требованиям CryptoStream).
|
||||||
|
public Stream CreateEncryptionStream(Stream unencryptedOutputStream, out byte[] iv)
|
||||||
|
{
|
||||||
|
var aes = Aes.Create();
|
||||||
|
aes.KeySize = 256;
|
||||||
|
aes.Key = _key;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.GenerateIV();
|
||||||
|
iv = aes.IV;
|
||||||
|
|
||||||
|
var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
||||||
|
return new CryptoStream(unencryptedOutputStream, encryptor, CryptoStreamMode.Write);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream CreateDecryptionStream(Stream encryptedInputStream, byte[] iv)
|
||||||
|
{
|
||||||
|
var aes = Aes.Create();
|
||||||
|
aes.KeySize = 256;
|
||||||
|
aes.Key = _key;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.IV = iv;
|
||||||
|
|
||||||
|
var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
||||||
|
return new CryptoStream(encryptedInputStream, decryptor, CryptoStreamMode.Read);
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
using Minio;
|
||||||
|
using Minio.DataModel.Args;
|
||||||
|
using Minio.Exceptions;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Knot.Shared.Kernel.Security;
|
||||||
|
|
||||||
|
namespace Knot.Shared.Kernel.Storage;
|
||||||
|
|
||||||
|
public class S3FileStorageService : IFileStorageService
|
||||||
|
{
|
||||||
|
private readonly IMinioClient _minioClient;
|
||||||
|
private readonly IEncryptionService _encryptionService;
|
||||||
|
private readonly string _bucketName;
|
||||||
|
|
||||||
|
public S3FileStorageService(IMinioClient minioClient, IEncryptionService encryptionService, string bucketName)
|
||||||
|
{
|
||||||
|
_minioClient = minioClient;
|
||||||
|
_encryptionService = encryptionService;
|
||||||
|
_bucketName = bucketName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureBucketExistsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bktExistArgs = new BucketExistsArgs().WithBucket(_bucketName);
|
||||||
|
bool found = await _minioClient.BucketExistsAsync(bktExistArgs).ConfigureAwait(false);
|
||||||
|
if (!found)
|
||||||
|
{
|
||||||
|
var mkBktArgs = new MakeBucketArgs().WithBucket(_bucketName);
|
||||||
|
await _minioClient.MakeBucketAsync(mkBktArgs).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (MinioException e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Bucket] Minio Error: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> UploadFileAsync(Stream fileStream, string fileName, string contentType)
|
||||||
|
{
|
||||||
|
await EnsureBucketExistsAsync();
|
||||||
|
|
||||||
|
// 1. Вычисляем SHA-256 для Content-Addressable Storage (CAS) - дедупликация
|
||||||
|
string fileHash;
|
||||||
|
using (var sha256 = SHA256.Create())
|
||||||
|
using (var msHash = new MemoryStream())
|
||||||
|
{
|
||||||
|
var startPos = fileStream.Position;
|
||||||
|
await fileStream.CopyToAsync(msHash);
|
||||||
|
var hashBytes = sha256.ComputeHash(msHash.ToArray());
|
||||||
|
fileHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||||
|
|
||||||
|
// Возвращаем указатель потока в начало для последующего чтения
|
||||||
|
fileStream.Position = startPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ext = Path.GetExtension(fileName);
|
||||||
|
var fileId = $"{fileHash}{ext}";
|
||||||
|
|
||||||
|
// 2. Шифруем файл "на лету" во временный файл
|
||||||
|
// Для больших файлов мы используем временный файл, чтобы не перегружать оперативную память (RAM)
|
||||||
|
var tempFilePath = Path.GetTempFileName();
|
||||||
|
byte[] ivParams;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var tempFs = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write))
|
||||||
|
using (var cryptoStream = _encryptionService.CreateEncryptionStream(tempFs, out ivParams))
|
||||||
|
{
|
||||||
|
await fileStream.CopyToAsync(cryptoStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Загружаем зашифрованный файл в MinIO
|
||||||
|
using var fileToUpload = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
var metadata = new System.Collections.Generic.Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "ContentType", contentType },
|
||||||
|
{ "OriginalFileName", fileName },
|
||||||
|
{ "IV", Convert.ToBase64String(ivParams) },
|
||||||
|
{ "KeyVersion", "1" },
|
||||||
|
{ "EncryptionAlgorithm", "AES-256-CBC" } // Как реализовано в потоке AES CBC
|
||||||
|
};
|
||||||
|
|
||||||
|
var putObjectArgs = new PutObjectArgs()
|
||||||
|
.WithBucket(_bucketName)
|
||||||
|
.WithObject(fileId)
|
||||||
|
.WithStreamData(fileToUpload)
|
||||||
|
.WithObjectSize(fileToUpload.Length)
|
||||||
|
.WithContentType("application/octet-stream")
|
||||||
|
.WithHeaders(metadata);
|
||||||
|
|
||||||
|
await _minioClient.PutObjectAsync(putObjectArgs).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return fileId;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(tempFilePath))
|
||||||
|
{
|
||||||
|
File.Delete(tempFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(Stream Stream, string ContentType, string FileName)> DownloadFileAsync(string fileId)
|
||||||
|
{
|
||||||
|
// Для скачивания, мы транслируем из Minio во временный файл, расшифровываем и возвращаем.
|
||||||
|
var tempFilePath = Path.GetTempFileName();
|
||||||
|
string contentType = "application/octet-stream";
|
||||||
|
string fileName = fileId;
|
||||||
|
string ivBase64 = null;
|
||||||
|
|
||||||
|
var statArgs = new StatObjectArgs().WithBucket(_bucketName).WithObject(fileId);
|
||||||
|
var stat = await _minioClient.StatObjectAsync(statArgs).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (stat.MetaData.ContainsKey("Contenttype")) contentType = stat.MetaData["Contenttype"];
|
||||||
|
if (stat.MetaData.ContainsKey("Originalfilename")) fileName = stat.MetaData["Originalfilename"];
|
||||||
|
if (stat.MetaData.ContainsKey("Iv")) ivBase64 = stat.MetaData["Iv"];
|
||||||
|
|
||||||
|
// Загружаем во временный файл (так как CryptoStream требует правильного чтения/записи)
|
||||||
|
var getObjArgs = new GetObjectArgs()
|
||||||
|
.WithBucket(_bucketName)
|
||||||
|
.WithObject(fileId)
|
||||||
|
.WithCallbackStream((stream) =>
|
||||||
|
{
|
||||||
|
using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write);
|
||||||
|
stream.CopyTo(fs);
|
||||||
|
});
|
||||||
|
|
||||||
|
await _minioClient.GetObjectAsync(getObjArgs).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Создаем поток дешифрования над байтами временного файла, оборачиваем в MemoryStream, чтобы он автоматически закрывался
|
||||||
|
// В реальном масштабируемом продакшене лучше возвращать FileStream напрямую, обернутый в crypto stream.
|
||||||
|
var msResult = new MemoryStream();
|
||||||
|
using (var fsRead = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read))
|
||||||
|
{
|
||||||
|
var iv = Convert.FromBase64String(ivBase64);
|
||||||
|
using var cryptoStream = _encryptionService.CreateDecryptionStream(fsRead, iv);
|
||||||
|
await cryptoStream.CopyToAsync(msResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Delete(tempFilePath);
|
||||||
|
msResult.Position = 0;
|
||||||
|
|
||||||
|
return (msResult, contentType, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Knot.Shared.Kernel;
|
namespace Knot.Shared.Kernel;
|
||||||
|
|
||||||
public class Class1
|
public class Class1
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Knot.Shared.Kernel.Security;
|
||||||
|
|
||||||
|
public interface IEncryptionService
|
||||||
|
{
|
||||||
|
// Шифрует текст (например, сообщения) для хранения в БД
|
||||||
|
string EncryptMessage(string plainText);
|
||||||
|
string DecryptMessage(string cipherText);
|
||||||
|
|
||||||
|
// Создает поток шифрования для потоковой передачи файлов "на лету"
|
||||||
|
Stream CreateEncryptionStream(Stream unencryptedOutputStream, out byte[] iv);
|
||||||
|
|
||||||
|
// Создает поток дешифрования для чтения файлов "на лету"
|
||||||
|
Stream CreateDecryptionStream(Stream encryptedInputStream, byte[] iv);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Knot.Shared.Kernel.Storage;
|
||||||
|
|
||||||
|
public interface IFileStorageService
|
||||||
|
{
|
||||||
|
// Загружает поток файла и возвращает его уникальный идентификатор (SHA256 хеш или GUID).
|
||||||
|
Task<string> UploadFileAsync(Stream stream, string fileName, string contentType);
|
||||||
|
|
||||||
|
// Скачивает файл и возвращает его расшифрованный поток и тип содержимого.
|
||||||
|
Task<(Stream Stream, string ContentType, string FileName)> DownloadFileAsync(string fileId);
|
||||||
|
}
|
||||||
@@ -367,9 +367,9 @@ export default function UserProfile({ userId, chatId, onClose, onGoToMessage, is
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: 50, filter: 'blur(10px)' }}
|
initial={{ opacity: 0, x: 50 }}
|
||||||
animate={{ opacity: 1, x: 0, filter: 'blur(0px)' }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
exit={{ opacity: 0, x: 50, filter: 'blur(10px)' }}
|
exit={{ opacity: 0, x: 50 }}
|
||||||
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
||||||
className="fixed right-3 top-3 bottom-3 w-[650px] max-w-[calc(100%-24px)] bg-surface-secondary/90 backdrop-blur-3xl shadow-2xl shadow-black/80 border border-white/5 rounded-[2rem] z-50 flex flex-col overflow-hidden ring-1 ring-white/10"
|
className="fixed right-3 top-3 bottom-3 w-[650px] max-w-[calc(100%-24px)] bg-surface-secondary/90 backdrop-blur-3xl shadow-2xl shadow-black/80 border border-white/5 rounded-[2rem] z-50 flex flex-col overflow-hidden ring-1 ring-white/10"
|
||||||
>
|
>
|
||||||
|
|||||||
+12
-12
@@ -244,12 +244,12 @@ const translations = {
|
|||||||
// Misc
|
// Misc
|
||||||
error: 'Ошибка',
|
error: 'Ошибка',
|
||||||
// Friends
|
// Friends
|
||||||
friends: 'Друзья',
|
friends: 'Контакты',
|
||||||
friendRequests: 'Заявки в друзья',
|
friendRequests: 'Заявки в контакты',
|
||||||
friendsList: 'Список друзей',
|
friendsList: 'Список контактов',
|
||||||
noFriends: 'Пока нет друзей',
|
noFriends: 'Пока нет контактов',
|
||||||
addFriend: 'Добавить в друзья',
|
addFriend: 'Добавить в контакты',
|
||||||
removeFriend: 'Удалить из друзей',
|
removeFriend: 'Удалить из контактов',
|
||||||
requestSent: 'Заявка отправлена',
|
requestSent: 'Заявка отправлена',
|
||||||
searchFriends: 'Поиск по @username (мин. 3 символа)',
|
searchFriends: 'Поиск по @username (мин. 3 символа)',
|
||||||
noSearchResults: 'Пользователи не найдены',
|
noSearchResults: 'Пользователи не найдены',
|
||||||
@@ -502,12 +502,12 @@ const translations = {
|
|||||||
searchGifs: 'Search GIFs...',
|
searchGifs: 'Search GIFs...',
|
||||||
trending: 'Trending',
|
trending: 'Trending',
|
||||||
error: 'Error',
|
error: 'Error',
|
||||||
friends: 'Friends',
|
friends: 'Contacts',
|
||||||
friendRequests: 'Friend requests',
|
friendRequests: 'Contact requests',
|
||||||
friendsList: 'Friends list',
|
friendsList: 'Contacts list',
|
||||||
noFriends: 'No friends yet',
|
noFriends: 'No contacts yet',
|
||||||
addFriend: 'Add friend',
|
addFriend: 'Add to contacts',
|
||||||
removeFriend: 'Remove friend',
|
removeFriend: 'Remove from contacts',
|
||||||
requestSent: 'Request sent',
|
requestSent: 'Request sent',
|
||||||
searchFriends: 'Search by @username (min. 3 chars)',
|
searchFriends: 'Search by @username (min. 3 chars)',
|
||||||
noSearchResults: 'No users found',
|
noSearchResults: 'No users found',
|
||||||
|
|||||||
@@ -60,21 +60,16 @@ export function connectSocket(token: string): SocketCompat {
|
|||||||
|
|
||||||
connection.start()
|
connection.start()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('[SignalR] Connected successfully');
|
|
||||||
console.log('[SignalR] Connection ID:', connection?.connectionId);
|
|
||||||
})
|
})
|
||||||
.catch(err => console.error('[SignalR] Connection error:', err.toString()));
|
.catch(err => console.error('[SignalR] Connection error:', err.toString()));
|
||||||
|
|
||||||
connection.onclose((error) => {
|
connection.onclose((error) => {
|
||||||
console.log('[SignalR] Connection closed', error);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onreconnecting((error) => {
|
connection.onreconnecting((error) => {
|
||||||
console.log('[SignalR] Reconnecting...', error);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onreconnected((connectionId) => {
|
connection.onreconnected((connectionId) => {
|
||||||
console.log('[SignalR] Reconnected, new connection ID:', connectionId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return socketWrapper;
|
return socketWrapper;
|
||||||
|
|||||||
+20
-1
@@ -27,12 +27,30 @@ services:
|
|||||||
TURN_URL: ${TURN_URL}
|
TURN_URL: ${TURN_URL}
|
||||||
TURN_USERNAME: ${TURN_USERNAME}
|
TURN_USERNAME: ${TURN_USERNAME}
|
||||||
TURN_PASSWORD: ${TURN_PASSWORD}
|
TURN_PASSWORD: ${TURN_PASSWORD}
|
||||||
|
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||||
|
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||||
|
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
||||||
|
S3_BUCKET: ${S3_BUCKET}
|
||||||
|
KNOT_MASTER_ENCRYPTION_KEY: ${KNOT_MASTER_ENCRYPTION_KEY}
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
- minio
|
||||||
ports:
|
ports:
|
||||||
- "5059:8080"
|
- "5059:8080"
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio
|
||||||
|
container_name: knot-minio
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${S3_ACCESS_KEY}
|
||||||
|
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY}
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
volumes:
|
volumes:
|
||||||
- ./apps/server-net/uploads:/app/uploads
|
- minio_data:/data
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
@@ -51,3 +69,4 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user