Шифрование и хранилище minio

This commit is contained in:
Халимов Рустам
2026-03-15 23:30:38 +03:00
parent 893c9f2316
commit 336f9ea559
60 changed files with 551 additions and 168 deletions

21
LICENSE
View File

@@ -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.

27
apps/server-net/out.txt Normal file
View File

@@ -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

23
apps/server-net/out8.txt Normal file
View File

@@ -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

View File

@@ -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
{
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)
{
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)
{
@@ -280,21 +290,16 @@ public sealed class ChatsController : ControllerBase
}
[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);
if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound();
if (avatar == null || avatar.Length == 0) return BadRequest("No file");
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars");
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
using var stream = avatar.OpenReadStream();
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);
chatRepository.Update(chat);
await uow.SaveChangesAsync(ct);
@@ -303,17 +308,13 @@ public sealed class ChatsController : ControllerBase
}
[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);
if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound();
if (avatar == null || avatar.Length == 0) return BadRequest("No file");
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars");
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
var fileName = $"{Guid.NewGuid()}.jpg";
var filePath = Path.Combine(uploadsPath, fileName);
string url;
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.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)
@@ -335,7 +343,6 @@ public sealed class ChatsController : ControllerBase
return StatusCode(500, "Error processing image: " + ex.Message);
}
var url = $"/uploads/avatars/{fileName}";
chat.UpdateAvatar(url);
chatRepository.Update(chat);
await uow.SaveChangesAsync(ct);

View File

@@ -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 });
}
}
}

View File

@@ -187,22 +187,14 @@ public sealed class MessagesController : ControllerBase
}
[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");
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads");
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var filePath = Path.Combine(uploadsPath, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
using var stream = file.OpenReadStream();
var fileId = await fileStorage.UploadFileAsync(stream, file.FileName, file.ContentType);
return Ok(new { url = $"/uploads/{fileName}", filename = file.FileName, size = file.Length });
return Ok(new { url = $"/api/files/{fileId}", filename = file.FileName, size = file.Length });
}
[HttpGet("chat/{chatId:guid}/shared")]

View File

@@ -64,45 +64,19 @@ public sealed class UsersController : ControllerBase
}
[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);
if (user == null) return NotFound();
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");
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
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}";
using var stream = fileToUpload.OpenReadStream();
var fileId = await fileStorage.UploadFileAsync(stream, fileToUpload.FileName, fileToUpload.ContentType);
avatarUrl = $"/api/files/{fileId}";
}
else
{
@@ -124,26 +98,20 @@ public sealed class UsersController : ControllerBase
}
[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);
if (user == null) return NotFound();
if (avatar == null || avatar.Length == 0) return BadRequest("No file");
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars");
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
var ext = ".jpg";
var fileName = $"{Guid.NewGuid()}{ext}";
var filePath = Path.Combine(uploadsPath, fileName);
string avatarUrl;
try
{
using (var inputStream = avatar.OpenReadStream())
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 startY = Math.Max(0, Math.Min(y, image.Height - 1));
int rectWidth = Math.Max(1, Math.Min(width, image.Width - startX));
@@ -151,8 +119,14 @@ public sealed class UsersController : ControllerBase
image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight)));
image.Mutate(ctx => ctx.Resize(400, 400));
using var outStream = new MemoryStream();
await image.SaveAsJpegAsync(outStream, ct);
outStream.Position = 0;
await image.SaveAsJpegAsync(filePath, ct);
var fileName = avatar.FileName ?? "avatar.jpg";
var id = await fileStorage.UploadFileAsync(outStream, fileName, "image/jpeg");
avatarUrl = $"/api/files/{id}";
}
}
catch (Exception ex)
@@ -160,7 +134,6 @@ public sealed class UsersController : ControllerBase
return StatusCode(500, "Error processing image: " + ex.Message);
}
var avatarUrl = $"/uploads/avatars/{fileName}";
user.UpdateAvatar(avatarUrl);
await _unitOfWork.SaveChangesAsync(ct);

View File

@@ -31,7 +31,12 @@ var envMappings = new Dictionary<string, string?>
["Jwt:Audience"] = builder.Configuration["JWT_AUDIENCE"],
["WebRtc:TurnUrl"] = builder.Configuration["TURN_URL"],
["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
@@ -41,7 +46,7 @@ builder.Configuration.AddInMemoryCollection(
builder.Services.AddIdentityModule(builder.Configuration);
builder.Services.AddChatsModule(builder.Configuration);
builder.Services.AddSharedInfrastructure();
builder.Services.AddSharedInfrastructure(builder.Configuration);
// Настройка CORS
builder.Services.AddCors(options =>
@@ -147,17 +152,7 @@ if (app.Environment.IsDevelopment())
// app.UseHttpsRedirection();
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads");
if (!Directory.Exists(uploadsPath))
{
Directory.CreateDirectory(uploadsPath);
}
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(uploadsPath),
RequestPath = "/uploads"
});
// Не раздаем статические файлы, так как теперь используем MinIO
app.UseAuthentication();
app.UseAuthorization();

View File

@@ -31,7 +31,7 @@ public sealed class CreateChatCommandHandler : ICommandHandler<CreateChatCommand
for (int i = 0; i < request.MemberIds.Count; 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);
}

View File

@@ -1,4 +1,4 @@
namespace Knot.Modules.Chats;
namespace Knot.Modules.Chats;
public class Class1
{

View File

@@ -13,13 +13,13 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence;
public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
{
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)
{
_mediator = mediator;
_encryptionService = encryptionService;
}
public DbSet<Chat> Chats => Set<Chat>();
@@ -59,6 +59,12 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
builder.ToTable("Messages");
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 =>
{

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
namespace Knot.Modules.Identity;
namespace Knot.Modules.Identity;
public class Class1
{

View File

@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Knot.Modules.Identity.Application.Abstractions;
using Knot.Modules.Identity.Domain;
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Security;
namespace Knot.Modules.Identity.Infrastructure.Persistence;
@@ -14,11 +15,13 @@ namespace Knot.Modules.Identity.Infrastructure.Persistence;
public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork
{
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)
{
_mediator = mediator;
_encryptionService = encryptionService;
}
public DbSet<User> Users => Set<User>();
@@ -52,6 +55,12 @@ public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork
builder.HasKey(s => s.Id);
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
builder.Metadata.FindNavigation(nameof(Story.Viewers))?.SetPropertyAccessMode(PropertyAccessMode.Field);
@@ -92,7 +101,14 @@ public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork
{
builder.ToTable("StoryReplies");
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 =>

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@@ -1,4 +1,4 @@
namespace Knot.Shared.Infrastructure;
namespace Knot.Shared.Infrastructure;
public class Class1
{

View File

@@ -1,15 +1,45 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Security;
using Knot.Shared.Kernel.Storage;
using Minio;
using System;
namespace Knot.Shared.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddSharedInfrastructure(this IServiceCollection services)
public static IServiceCollection AddSharedInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddHttpContextAccessor();
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;
}
}

View File

@@ -12,6 +12,7 @@
<ItemGroup>
<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="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -1,4 +1,4 @@
namespace Knot.Shared.Kernel;
namespace Knot.Shared.Kernel;
public class Class1
{

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -367,9 +367,9 @@ export default function UserProfile({ userId, chatId, onClose, onGoToMessage, is
onClick={onClose}
/>
<motion.div
initial={{ opacity: 0, x: 50, filter: 'blur(10px)' }}
animate={{ opacity: 1, x: 0, filter: 'blur(0px)' }}
exit={{ opacity: 0, x: 50, filter: 'blur(10px)' }}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 50 }}
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"
>

View File

@@ -244,12 +244,12 @@ const translations = {
// Misc
error: 'Ошибка',
// Friends
friends: 'Друзья',
friendRequests: 'Заявки в друзья',
friendsList: 'Список друзей',
noFriends: 'Пока нет друзей',
addFriend: 'Добавить в друзья',
removeFriend: 'Удалить из друзей',
friends: 'Контакты',
friendRequests: 'Заявки в контакты',
friendsList: 'Список контактов',
noFriends: 'Пока нет контактов',
addFriend: 'Добавить в контакты',
removeFriend: 'Удалить из контактов',
requestSent: 'Заявка отправлена',
searchFriends: 'Поиск по @username (мин. 3 символа)',
noSearchResults: 'Пользователи не найдены',
@@ -502,12 +502,12 @@ const translations = {
searchGifs: 'Search GIFs...',
trending: 'Trending',
error: 'Error',
friends: 'Friends',
friendRequests: 'Friend requests',
friendsList: 'Friends list',
noFriends: 'No friends yet',
addFriend: 'Add friend',
removeFriend: 'Remove friend',
friends: 'Contacts',
friendRequests: 'Contact requests',
friendsList: 'Contacts list',
noFriends: 'No contacts yet',
addFriend: 'Add to contacts',
removeFriend: 'Remove from contacts',
requestSent: 'Request sent',
searchFriends: 'Search by @username (min. 3 chars)',
noSearchResults: 'No users found',

View File

@@ -60,21 +60,16 @@ export function connectSocket(token: string): SocketCompat {
connection.start()
.then(() => {
console.log('[SignalR] Connected successfully');
console.log('[SignalR] Connection ID:', connection?.connectionId);
})
.catch(err => console.error('[SignalR] Connection error:', err.toString()));
connection.onclose((error) => {
console.log('[SignalR] Connection closed', error);
});
connection.onreconnecting((error) => {
console.log('[SignalR] Reconnecting...', error);
});
connection.onreconnected((connectionId) => {
console.log('[SignalR] Reconnected, new connection ID:', connectionId);
});
return socketWrapper;

BIN
diff.txt Normal file

Binary file not shown.

View File

@@ -27,12 +27,30 @@ services:
TURN_URL: ${TURN_URL}
TURN_USERNAME: ${TURN_USERNAME}
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:
- db
- minio
ports:
- "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:
- ./apps/server-net/uploads:/app/uploads
- minio_data:/data
web:
build:
@@ -51,3 +69,4 @@ services:
volumes:
postgres_data:
minio_data:

BIN
logs.txt Normal file

Binary file not shown.

BIN
logs2.txt Normal file

Binary file not shown.