Доработки профиля

This commit is contained in:
Халимов Рустам
2026-03-14 01:59:06 +03:00
parent 010b96d362
commit 15344f8636
69 changed files with 1625 additions and 561 deletions
@@ -28,9 +28,11 @@ public sealed class CreateChatCommandHandler : ICommandHandler<CreateChatCommand
{
var chat = Chat.Create(request.Name, request.Type);
foreach (var userId in request.MemberIds)
for (int i = 0; i < request.MemberIds.Count; i++)
{
chat.AddMember(userId, ChatRole.Member);
var userId = request.MemberIds[i];
var role = (i == 0 && request.Type == ChatType.Group) ? ChatRole.Admin : ChatRole.Member;
chat.AddMember(userId, role);
}
_chatRepository.Add(chat);
@@ -32,17 +32,19 @@ public sealed class Chat : AggregateRoot<Guid>
{
public ChatType Type { get; private set; }
public string? Name { get; private set; }
public string? Description { get; private set; }
public string? Avatar { get; private set; }
public DateTime CreatedAt { get; private set; }
private readonly List<ChatMember> _members = new();
public IReadOnlyCollection<ChatMember> Members => _members.AsReadOnly();
private Chat(Guid id, ChatType type, string? name, string? avatar) : base(id)
private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null) : base(id)
{
Type = type;
Name = name;
Avatar = avatar;
Description = description;
CreatedAt = DateTime.UtcNow;
}
@@ -69,9 +71,9 @@ public sealed class Chat : AggregateRoot<Guid>
/// <summary>
/// Фабричный метод для создания чата.
/// </summary>
public static Chat Create(string? name, ChatType type, string? avatar = null)
public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null)
{
var chat = new Chat(Guid.NewGuid(), type, name, avatar);
var chat = new Chat(Guid.NewGuid(), type, name, avatar, description);
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
return chat;
}
@@ -91,6 +93,8 @@ public sealed class Chat : AggregateRoot<Guid>
public void UpdateName(string name) => Name = name;
public void UpdateDescription(string? description) => Description = description;
public void UpdateAvatar(string? avatarUrl) => Avatar = avatarUrl;
}
@@ -90,8 +90,10 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
Size = m.Size
}).ToList(),
Sender = senderObj,
Reactions = new List<object>(),
ReadBy = new List<object>()
ReadBy = new List<object>(),
message.StoryId,
message.StoryMediaUrl,
message.StoryMediaType
}, cancellationToken);
}
}
@@ -0,0 +1,263 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Vortex.Modules.Chats.Infrastructure.Persistence;
#nullable disable
namespace Vortex.Modules.Chats.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(ChatsDbContext))]
[Migration("20260313204219_AddChatDescription")]
partial class AddChatDescription
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("chats")
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Chat", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Avatar")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Chats", "chats");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Message", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ChatId")
.HasColumnType("uuid");
b.Property<string>("Content")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
.IsRequired()
.HasColumnType("uuid[]")
.HasColumnName("DeletedByUsers");
b.Property<Guid?>("ForwardedFromId")
.HasColumnType("uuid");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsEdited")
.HasColumnType("boolean");
b.Property<string>("Quote")
.HasColumnType("text");
b.Property<Guid?>("ReplyToId")
.HasColumnType("uuid");
b.Property<Guid>("SenderId")
.HasColumnType("uuid");
b.Property<Guid?>("StoryId")
.HasColumnType("uuid");
b.Property<string>("StoryMediaType")
.HasColumnType("text");
b.Property<string>("StoryMediaUrl")
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Messages", "chats");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Reaction", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Emoji")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("MessageId", "UserId", "Emoji")
.IsUnique();
b.ToTable("MessageReactions", "chats");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.ReadReceipt", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<DateTime>("ReadAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("MessageId", "UserId")
.IsUnique();
b.ToTable("ReadReceipts", "chats");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Chat", b =>
{
b.OwnsMany("Vortex.Modules.Chats.Domain.ChatMember", "Members", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b1.Property<Guid>("ChatId")
.HasColumnType("uuid");
b1.Property<bool>("IsMuted")
.HasColumnType("boolean");
b1.Property<bool>("IsPinned")
.HasColumnType("boolean");
b1.Property<DateTime>("JoinedAt")
.HasColumnType("timestamp with time zone");
b1.Property<string>("Role")
.IsRequired()
.HasColumnType("text");
b1.Property<Guid>("UserId")
.HasColumnType("uuid");
b1.HasKey("Id");
b1.HasIndex("ChatId", "UserId")
.IsUnique();
b1.ToTable("ChatMembers", "chats");
b1.WithOwner()
.HasForeignKey("ChatId");
});
b.Navigation("Members");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Message", b =>
{
b.OwnsMany("Vortex.Modules.Chats.Domain.Media", "Media", b1 =>
{
b1.Property<Guid>("MessageId")
.HasColumnType("uuid");
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b1.Property<string>("Filename")
.HasColumnType("text");
b1.Property<long?>("Size")
.HasColumnType("bigint");
b1.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b1.HasKey("MessageId", "Id");
b1.ToTable("MessageMedia", "chats");
b1.WithOwner()
.HasForeignKey("MessageId");
});
b.Navigation("Media");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Reaction", b =>
{
b.HasOne("Vortex.Modules.Chats.Domain.Message", null)
.WithMany("Reactions")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.ReadReceipt", b =>
{
b.HasOne("Vortex.Modules.Chats.Domain.Message", null)
.WithMany("ReadBy")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Message", b =>
{
b.Navigation("Reactions");
b.Navigation("ReadBy");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Vortex.Modules.Chats.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddChatDescription : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Description",
schema: "chats",
table: "Chats",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Description",
schema: "chats",
table: "Chats");
}
}
}
@@ -35,6 +35,9 @@ namespace Vortex.Modules.Chats.Infrastructure.Persistence.Migrations
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
@@ -383,8 +383,9 @@ public sealed class ChatHub : Hub
}
[HubMethodName("group_call_leave")]
public async Task GroupCallLeave(string chatId)
public async Task GroupCallLeave(GroupLeaveRequest request)
{
var chatId = request.ChatId;
var userId = _userContext.UserId.ToString();
if (_groupCallParticipants.TryGetValue(chatId, out var participants))
{
@@ -457,6 +458,84 @@ public sealed class ChatHub : Hub
});
}
[HubMethodName("group_call_status")]
public async Task GroupCallStatus(GroupCallStatusRequest request)
{
var chatId = request.ChatId;
var userId = _userContext.UserId.ToString();
if (_groupCallParticipants.TryGetValue(chatId, out var participants))
{
if (participants.TryGetValue(userId, out var info))
{
// Update local state if needed (e.g. muted status)
participants[userId] = info with
{
IsMuted = request.IsMuted,
IsVideoOff = request.IsVideoOff
};
}
}
await Clients.Group(request.ChatId).SendAsync("group_call_status_updated", new {
chatId = request.ChatId,
userId = Context.UserIdentifier,
isMuted = request.IsMuted,
isVideoOff = request.IsVideoOff
});
}
[HubMethodName("group_call_status_params")]
public async Task GroupCallStatusParams(string chatId, bool isMuted, bool isVideoOff)
{
var userId = _userContext.UserId.ToString();
if (_groupCallParticipants.TryGetValue(chatId, out var participants))
{
if (participants.TryGetValue(userId, out var info))
{
// Update local state if needed (e.g. muted status)
participants[userId] = info with
{
IsMuted = isMuted,
IsVideoOff = isVideoOff
};
}
}
await Clients.Group(chatId).SendAsync("group_call_status_updated", new {
chatId = chatId,
userId = Context.UserIdentifier,
isMuted = isMuted,
isVideoOff = isVideoOff
});
}
[HubMethodName("get_group_call_status")]
public async Task GetGroupCallStatus(GetGroupCallStatusRequest request)
{
var chatId = request.ChatId;
if (_groupCallParticipants.TryGetValue(chatId, out var participants))
{
var others = participants.Values.ToList();
_logger.LogInformation("Found {Count} participants for chat {ChatId}", others.Count, chatId);
await Clients.Caller.SendAsync("group_call_active", new
{
chatId = chatId,
participants = others.Select(p => p.Id).ToList()
});
}
else
{
_logger.LogInformation("No active call for chat {ChatId}", chatId);
await Clients.Caller.SendAsync("group_call_active", new
{
chatId = chatId,
participants = new List<string>()
});
}
}
[HubMethodName("screen_share_started")]
public async Task ScreenShareStarted(string chatId)
{
@@ -523,7 +602,10 @@ public sealed class ChatHub : Hub
public record RemoveReactionRequest(Guid MessageId, Guid ChatId, string Emoji);
public record DeleteMessagesHubRequest(Guid ChatId, List<string> MessageIds, bool DeleteForAll);
public record GroupCallJoinRequest(string ChatId, string CallType);
public record ParticipantInfo(string Id, string Username, string DisplayName, string? Avatar, bool IsSharingScreen = false);
public record ParticipantInfo(string Id, string Username, string DisplayName, string? Avatar, bool IsSharingScreen = false, bool IsMuted = false, bool IsVideoOff = false);
public record GroupLeaveRequest(string ChatId);
public record GetGroupCallStatusRequest(string ChatId);
public record GroupCallStatusRequest(string ChatId, bool IsMuted, bool IsVideoOff);
public record GroupCallOfferRequest(string ChatId, string TargetUserId, object Offer);
public record GroupCallAnswerRequest(string ChatId, string TargetUserId, object Answer);
public record GroupIceCandidateRequest(string ChatId, string TargetUserId, object Candidate);
@@ -12,6 +12,8 @@ using Vortex.Shared.Kernel;
using Vortex.Modules.Identity.Domain;
using Vortex.Modules.Identity.Application.Abstractions;
using Vortex.Modules.Chats.Infrastructure.Persistence;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
namespace Vortex.Host.Controllers;
@@ -100,6 +102,9 @@ public sealed class ChatsController : ControllerBase
m.Type,
m.ReplyToId,
m.Quote,
m.StoryId,
m.StoryMediaUrl,
m.StoryMediaType,
m.IsEdited,
m.IsDeleted,
m.CreatedAt,
@@ -119,7 +124,8 @@ public sealed class ChatsController : ControllerBase
{
id = c.Id,
type = c.Type.ToString().ToLowerInvariant(),
name = c.Type == ChatType.Favorites ? "Избранное" : c.Name,
name = c.Type == ChatType.Favorites ? "Избранное" : (c.Type == ChatType.Personal ? null : c.Name),
description = c.Description,
avatar = c.Avatar,
createdAt = c.CreatedAt,
members = members,
@@ -160,10 +166,11 @@ public sealed class ChatsController : ControllerBase
public async Task<IActionResult> CreateGroup([FromBody] CreateGroupChatRequest request, CancellationToken ct)
{
var memberIds = request.MemberIds.ToList();
if (!memberIds.Contains(_userContext.UserId))
if (memberIds.Contains(_userContext.UserId))
{
memberIds.Add(_userContext.UserId);
memberIds.Remove(_userContext.UserId);
}
memberIds.Insert(0, _userContext.UserId);
var command = new CreateChatCommand(request.Name, ChatType.Group, memberIds);
Result<Guid> result = await _sender.Send(command, ct);
@@ -191,6 +198,7 @@ public sealed class ChatsController : ControllerBase
if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound();
if (request.Name != null) chat.UpdateName(request.Name);
if (request.Description != null) chat.UpdateDescription(request.Description);
chatRepository.Update(chat);
await uow.SaveChangesAsync(ct);
@@ -294,6 +302,47 @@ public sealed class ChatsController : ControllerBase
return Ok(await MapChatAsync(id, ct));
}
[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)
{
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);
try
{
using (var inputStream = avatar.OpenReadStream())
using (var image = await SixLabors.ImageSharp.Image.LoadAsync(inputStream))
{
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));
int rectHeight = Math.Max(1, Math.Min(height, image.Height - startY));
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);
}
}
catch (Exception ex)
{
return StatusCode(500, "Error processing image: " + ex.Message);
}
var url = $"/uploads/avatars/{fileName}";
chat.UpdateAvatar(url);
chatRepository.Update(chat);
await uow.SaveChangesAsync(ct);
return Ok(await MapChatAsync(id, ct));
}
[HttpDelete("{id:guid}/avatar")]
public async Task<IActionResult> RemoveGroupAvatar(Guid id, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, CancellationToken ct)
{
@@ -310,6 +359,7 @@ public sealed class ChatsController : ControllerBase
private async Task<object> MapChatAsync(Guid chatId, CancellationToken ct)
{
var chatRepository = HttpContext.RequestServices.GetRequiredService<IChatRepository>();
var messageRepository = HttpContext.RequestServices.GetRequiredService<IMessageRepository>();
var chat = await chatRepository.GetByIdAsync(chatId, ct);
if (chat == null) return new { };
@@ -334,15 +384,48 @@ public sealed class ChatsController : ControllerBase
});
}
var chatMessages = await messageRepository.GetChatMessagesAsync(chatId, 1, 0, ct);
var messagesList = new List<object>();
if (chatMessages.Any())
{
var m = chatMessages.First();
var senderObj = await _userRepository.GetByIdAsync(m.SenderId, ct);
messagesList.Add(new
{
m.Id,
m.ChatId,
m.SenderId,
m.Content,
m.Type,
m.ReplyToId,
m.Quote,
m.StoryId,
m.StoryMediaUrl,
m.StoryMediaType,
m.IsEdited,
m.IsDeleted,
m.CreatedAt,
Media = m.Media.ToList(),
Sender = senderObj != null ? new {
id = senderObj.Id,
username = senderObj.Username,
displayName = senderObj.DisplayName,
avatar = senderObj.Avatar
} : new { id = m.SenderId, username = "unknown", displayName = "Unknown", avatar = (string?)null },
ReadBy = m.ReadBy.Select(r => new { userId = r.UserId }).ToList()
});
}
return new
{
id = chat.Id,
type = chat.Type.ToString().ToLowerInvariant(),
name = chat.Type == ChatType.Favorites ? "Избранное" : chat.Name,
name = chat.Type == ChatType.Favorites ? "Избранное" : (chat.Type == ChatType.Personal ? null : chat.Name),
description = chat.Description,
avatar = chat.Avatar,
createdAt = chat.CreatedAt,
members = members,
messages = new List<object>(),
messages = messagesList,
unreadCount = 0
};
}
@@ -351,6 +434,6 @@ public sealed class ChatsController : ControllerBase
public sealed record CreateChatRequest(string Name, ChatType Type, List<Guid> MemberIds);
public sealed record CreatePersonalChatRequest(Guid UserId);
public sealed record CreateGroupChatRequest(string Name, List<Guid> MemberIds);
public sealed record UpdateChatRequest(string? Name);
public sealed record UpdateChatRequest(string? Name, string? Description);
public sealed record AddMembersRequest(List<Guid> UserIds);
@@ -103,6 +103,8 @@ public sealed class MessagesController : ControllerBase
avatar = fwd.Avatar
} : null,
storyId = m.StoryId,
storyMediaUrl = m.StoryMediaUrl,
storyMediaType = m.StoryMediaType,
media = m.Media.Select(media => new {
media.Id,
media.Type,
@@ -161,6 +163,9 @@ public sealed class MessagesController : ControllerBase
displayName = fwd.DisplayName,
avatar = fwd.Avatar
} : null,
storyId = m.StoryId,
storyMediaUrl = m.StoryMediaUrl,
storyMediaType = m.StoryMediaType,
media = m.Media.Select(media => new {
media.Id,
media.Type,
@@ -217,7 +222,7 @@ public sealed class MessagesController : ControllerBase
{
var linkRegex = new Regex(@"https?://[^\s]+", RegexOptions.IgnoreCase);
var contentLinks = !string.IsNullOrEmpty(m.Content) ? linkRegex.Matches(m.Content).Select(match => match.Value).ToList() : new List<string>();
var mediaLinks = m.Media.Where(media => media.Type?.ToLower() == "link").Select(media => media.Url).ToList();
var mediaLinks = (m.Media ?? Enumerable.Empty<Media>()).Where(media => media.Type?.ToLower() == "link").Select(media => media.Url).ToList();
var allLinks = contentLinks.Concat(mediaLinks).Distinct().ToList();
if (allLinks.Any())
@@ -249,9 +254,12 @@ public sealed class MessagesController : ControllerBase
var sender = await _userRepository.GetByIdAsync(m.SenderId, ct);
result.Add(new
{
m.Id,
m.ChatId,
m.SenderId,
m.ReplyToId,
m.Quote,
m.StoryId,
m.StoryMediaUrl,
m.StoryMediaType,
m.IsEdited,
m.Content,
m.Type,
m.CreatedAt,
@@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Authorization;
using Vortex.Modules.Identity.Domain;
using Vortex.Shared.Kernel;
using Vortex.Modules.Identity.Application.Abstractions;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
namespace Vortex.Host.Controllers;
@@ -121,6 +123,58 @@ 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)
{
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);
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));
int rectHeight = Math.Max(1, Math.Min(height, image.Height - startY));
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);
}
}
catch (Exception ex)
{
return StatusCode(500, "Error processing image: " + ex.Message);
}
var avatarUrl = $"/uploads/avatars/{fileName}";
user.UpdateAvatar(avatarUrl);
await _unitOfWork.SaveChangesAsync(ct);
return Ok(new {
id = user.Id,
username = user.Username,
displayName = user.DisplayName,
avatar = user.Avatar,
bio = user.Bio,
birthday = user.Birthday,
createdAt = user.CreatedAt
});
}
[HttpDelete("avatar")]
public async Task<IActionResult> DeleteAvatar(CancellationToken ct)
{
@@ -18,6 +18,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.5" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
@@ -17,6 +17,7 @@
"Microsoft.EntityFrameworkCore.Design": "10.0.4",
"Microsoft.IdentityModel.Tokens": "8.16.0",
"Npgsql.EntityFrameworkCore.PostgreSQL": "10.0.0",
"SixLabors.ImageSharp": "3.1.12",
"Swashbuckle.AspNetCore": "10.1.5",
"System.IdentityModel.Tokens.Jwt": "8.16.0",
"Vortex.Modules.Chats": "1.0.0",
@@ -741,6 +742,14 @@
}
}
},
"SixLabors.ImageSharp/3.1.12": {
"runtime": {
"lib/net6.0/SixLabors.ImageSharp.dll": {
"assemblyVersion": "3.0.0.0",
"fileVersion": "3.1.12.0"
}
}
},
"Swashbuckle.AspNetCore/10.1.5": {
"dependencies": {
"Swashbuckle.AspNetCore.Swagger": "10.1.5",
@@ -1252,6 +1261,13 @@
"path": "npgsql.entityframeworkcore.postgresql/10.0.0",
"hashPath": "npgsql.entityframeworkcore.postgresql.10.0.0.nupkg.sha512"
},
"SixLabors.ImageSharp/3.1.12": {
"type": "package",
"serviceable": true,
"sha512": "sha512-iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==",
"path": "sixlabors.imagesharp/3.1.12",
"hashPath": "sixlabors.imagesharp.3.1.12.nupkg.sha512"
},
"Swashbuckle.AspNetCore/10.1.5": {
"type": "package",
"serviceable": true,
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Vortex.Host")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+ca1d88191ca628dbe2371abab56fb3cdfe386d65")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+010b96d362a728b28bae56836e464103371899f4")]
[assembly: System.Reflection.AssemblyProductAttribute("Vortex.Host")]
[assembly: System.Reflection.AssemblyTitleAttribute("Vortex.Host")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
f3d52bfd2b858d5d383380a9e4100efe419d613a4ecf13cf06c4ad546122d06b
42cfa915aa235da6705c7cf95cb1a14d6010145730d282552fab9d0c3500b1eb
@@ -1 +1 @@
a0a268746b849e72488e8608dc7529251d735d933091a22e5f93d38662ef7534
d31a690f913188ddcf07b749ff1ed53974efbd88f60dacca614c62e22165ddc7
@@ -158,3 +158,4 @@ E:\GIT\forkmessager\apps\server-net\src\Vortex.Host\bin\Debug\net10.0\System.Ide
E:\GIT\forkmessager\apps\server-net\src\Vortex.Host\bin\Debug\net10.0\Microsoft.AspNetCore.Authentication.JwtBearer.dll
E:\GIT\forkmessager\apps\server-net\src\Vortex.Host\bin\Debug\net10.0\Microsoft.IdentityModel.Protocols.dll
E:\GIT\forkmessager\apps\server-net\src\Vortex.Host\bin\Debug\net10.0\Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
E:\GIT\forkmessager\apps\server-net\src\Vortex.Host\bin\Debug\net10.0\SixLabors.ImageSharp.dll
@@ -1 +1 @@
{"GlobalPropertiesHash":"2XkPbXFkjVRjCUBTJbQq8CDd7pjsUOS1j3gPHS73xDA=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["uOUIBvk7jdHp7c93RIuECwNykpOmiZnVmQxETsngeR0=","ToV71GGCtcAptkaGqQYXeNtDQnAJEljvYAkAKnIRZ\u002Bs=","2fjU\u002BjCBiHEuKj2uQgoWti0oZXkpAdICFty/O9cJSyc=","aTLUi985DoL1wb7QmuhiHoO7J3hi30WEVAUbYVkXxss=","GsJcYsaPdTiZszOuaIQXvBoHa2G\u002B8z3ZDvEk9/eI47g="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"2XkPbXFkjVRjCUBTJbQq8CDd7pjsUOS1j3gPHS73xDA=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["uOUIBvk7jdHp7c93RIuECwNykpOmiZnVmQxETsngeR0=","ToV71GGCtcAptkaGqQYXeNtDQnAJEljvYAkAKnIRZ\u002Bs=","2fjU\u002BjCBiHEuKj2uQgoWti0oZXkpAdICFty/O9cJSyc=","aTLUi985DoL1wb7QmuhiHoO7J3hi30WEVAUbYVkXxss=","zR9GPKJ4P6VsVoHQ2DUOzlUgEbOZ6Jtpw3dF8z1T3G8="],"CachedAssets":{},"CachedCopyCandidates":{}}
@@ -1 +1 @@
{"GlobalPropertiesHash":"2L4HBRt/ChGO9rohi3wtSDqo95X47fme5m8OHgJIZL0=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["uOUIBvk7jdHp7c93RIuECwNykpOmiZnVmQxETsngeR0=","ToV71GGCtcAptkaGqQYXeNtDQnAJEljvYAkAKnIRZ\u002Bs=","2fjU\u002BjCBiHEuKj2uQgoWti0oZXkpAdICFty/O9cJSyc=","aTLUi985DoL1wb7QmuhiHoO7J3hi30WEVAUbYVkXxss=","GsJcYsaPdTiZszOuaIQXvBoHa2G\u002B8z3ZDvEk9/eI47g="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"2L4HBRt/ChGO9rohi3wtSDqo95X47fme5m8OHgJIZL0=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["uOUIBvk7jdHp7c93RIuECwNykpOmiZnVmQxETsngeR0=","ToV71GGCtcAptkaGqQYXeNtDQnAJEljvYAkAKnIRZ\u002Bs=","2fjU\u002BjCBiHEuKj2uQgoWti0oZXkpAdICFty/O9cJSyc=","aTLUi985DoL1wb7QmuhiHoO7J3hi30WEVAUbYVkXxss=","zR9GPKJ4P6VsVoHQ2DUOzlUgEbOZ6Jtpw3dF8z1T3G8="],"CachedAssets":{},"CachedCopyCandidates":{}}
@@ -1994,6 +1994,10 @@
"target": "Package",
"version": "[10.0.0, )"
},
"SixLabors.ImageSharp": {
"target": "Package",
"version": "[3.1.12, )"
},
"Swashbuckle.AspNetCore": {
"target": "Package",
"version": "[10.1.5, )"
@@ -17,6 +17,7 @@
<Import Project="$(NuGetPackageRoot)microsoft.entityframeworkcore\10.0.4\buildTransitive\net10.0\Microsoft.EntityFrameworkCore.props" Condition="Exists('$(NuGetPackageRoot)microsoft.entityframeworkcore\10.0.4\buildTransitive\net10.0\Microsoft.EntityFrameworkCore.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.apidescription.server\10.0.0\build\Microsoft.Extensions.ApiDescription.Server.props" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.apidescription.server\10.0.0\build\Microsoft.Extensions.ApiDescription.Server.props')" />
<Import Project="$(NuGetPackageRoot)swashbuckle.aspnetcore\10.1.5\build\Swashbuckle.AspNetCore.props" Condition="Exists('$(NuGetPackageRoot)swashbuckle.aspnetcore\10.1.5\build\Swashbuckle.AspNetCore.props')" />
<Import Project="$(NuGetPackageRoot)sixlabors.imagesharp\3.1.12\build\SixLabors.ImageSharp.props" Condition="Exists('$(NuGetPackageRoot)sixlabors.imagesharp\3.1.12\build\SixLabors.ImageSharp.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.codeanalysis.analyzers\3.11.0\buildTransitive\Microsoft.CodeAnalysis.Analyzers.props" Condition="Exists('$(NuGetPackageRoot)microsoft.codeanalysis.analyzers\3.11.0\buildTransitive\Microsoft.CodeAnalysis.Analyzers.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.entityframeworkcore.design\10.0.4\build\net10.0\Microsoft.EntityFrameworkCore.Design.props" Condition="Exists('$(NuGetPackageRoot)microsoft.entityframeworkcore.design\10.0.4\build\net10.0\Microsoft.EntityFrameworkCore.Design.props')" />
</ImportGroup>
@@ -1022,6 +1022,22 @@
}
}
},
"SixLabors.ImageSharp/3.1.12": {
"type": "package",
"compile": {
"lib/net6.0/SixLabors.ImageSharp.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net6.0/SixLabors.ImageSharp.dll": {
"related": ".xml"
}
},
"build": {
"build/SixLabors.ImageSharp.props": {}
}
},
"Swashbuckle.AspNetCore/10.1.5": {
"type": "package",
"dependencies": {
@@ -3641,6 +3657,22 @@
"postgresql.png"
]
},
"SixLabors.ImageSharp/3.1.12": {
"sha512": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==",
"type": "package",
"path": "sixlabors.imagesharp/3.1.12",
"files": [
".nupkg.metadata",
".signature.p7s",
"LICENSE",
"build/SixLabors.ImageSharp.props",
"lib/net6.0/SixLabors.ImageSharp.dll",
"lib/net6.0/SixLabors.ImageSharp.xml",
"sixlabors.imagesharp.128.png",
"sixlabors.imagesharp.3.1.12.nupkg.sha512",
"sixlabors.imagesharp.nuspec"
]
},
"Swashbuckle.AspNetCore/10.1.5": {
"sha512": "/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==",
"type": "package",
@@ -3962,6 +3994,7 @@
"Microsoft.EntityFrameworkCore.Design >= 10.0.4",
"Microsoft.IdentityModel.Tokens >= 8.16.0",
"Npgsql.EntityFrameworkCore.PostgreSQL >= 10.0.0",
"SixLabors.ImageSharp >= 3.1.12",
"Swashbuckle.AspNetCore >= 10.1.5",
"System.IdentityModel.Tokens.Jwt >= 8.16.0",
"Vortex.Modules.Chats >= 1.0.0",
@@ -4068,6 +4101,10 @@
"target": "Package",
"version": "[10.0.0, )"
},
"SixLabors.ImageSharp": {
"target": "Package",
"version": "[3.1.12, )"
},
"Swashbuckle.AspNetCore": {
"target": "Package",
"version": "[10.1.5, )"
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "Lr9L1t57HIQ=",
"dgSpecHash": "ghRzA2bAmsQ=",
"success": true,
"projectFilePath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Vortex.Host\\Vortex.Host.csproj",
"expectedPackageFiles": [
@@ -53,6 +53,7 @@
"C:\\Users\\HomePC\\.nuget\\packages\\newtonsoft.json\\13.0.3\\newtonsoft.json.13.0.3.nupkg.sha512",
"C:\\Users\\HomePC\\.nuget\\packages\\npgsql\\10.0.0\\npgsql.10.0.0.nupkg.sha512",
"C:\\Users\\HomePC\\.nuget\\packages\\npgsql.entityframeworkcore.postgresql\\10.0.0\\npgsql.entityframeworkcore.postgresql.10.0.0.nupkg.sha512",
"C:\\Users\\HomePC\\.nuget\\packages\\sixlabors.imagesharp\\3.1.12\\sixlabors.imagesharp.3.1.12.nupkg.sha512",
"C:\\Users\\HomePC\\.nuget\\packages\\swashbuckle.aspnetcore\\10.1.5\\swashbuckle.aspnetcore.10.1.5.nupkg.sha512",
"C:\\Users\\HomePC\\.nuget\\packages\\swashbuckle.aspnetcore.swagger\\10.1.5\\swashbuckle.aspnetcore.swagger.10.1.5.nupkg.sha512",
"C:\\Users\\HomePC\\.nuget\\packages\\swashbuckle.aspnetcore.swaggergen\\10.1.5\\swashbuckle.aspnetcore.swaggergen.10.1.5.nupkg.sha512",
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB