diff --git a/dotnet/AipsCore/Application/Common/Message/AddRectangle/AddRectangleMessage.cs b/dotnet/AipsCore/Application/Common/Message/AddRectangle/AddRectangleMessage.cs new file mode 100644 index 0000000..0277ee1 --- /dev/null +++ b/dotnet/AipsCore/Application/Common/Message/AddRectangle/AddRectangleMessage.cs @@ -0,0 +1,6 @@ +using AipsCore.Application.Abstract.MessageBroking; +using AipsCore.Application.Models.Shape.Command.CreateRectangle; + +namespace AipsCore.Application.Common.Message.AddRectangle; + +public record AddRectangleMessage(CreateRectangleCommand Command) : IMessage; \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Common/Message/AddRectangle/AddRectangleMessageHandler.cs b/dotnet/AipsCore/Application/Common/Message/AddRectangle/AddRectangleMessageHandler.cs new file mode 100644 index 0000000..44875c5 --- /dev/null +++ b/dotnet/AipsCore/Application/Common/Message/AddRectangle/AddRectangleMessageHandler.cs @@ -0,0 +1,19 @@ +using AipsCore.Application.Abstract; +using AipsCore.Application.Abstract.MessageBroking; + +namespace AipsCore.Application.Common.Message.AddRectangle; + +public class AddRectangleMessageHandler : IMessageHandler +{ + private readonly IDispatcher _dispatcher; + + public AddRectangleMessageHandler(IDispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + public async Task Handle(AddRectangleMessage message, CancellationToken cancellationToken) + { + await _dispatcher.Execute(message.Command, cancellationToken); + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommand.cs b/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommand.cs index 84602d6..288d735 100644 --- a/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommand.cs +++ b/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommand.cs @@ -4,6 +4,7 @@ using AipsCore.Domain.Models.Shape.ValueObjects; namespace AipsCore.Application.Models.Shape.Command.CreateRectangle; public record CreateRectangleCommand( + string Id, string WhiteboardId, string AuthorId, int PositionX, @@ -11,4 +12,4 @@ public record CreateRectangleCommand( string Color, int EndPositionX, int EndPositionY, - int BorderThickness) : ICommand; \ No newline at end of file + int BorderThickness) : ICommand; \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommandHandler.cs b/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommandHandler.cs index d0352f8..543a2a1 100644 --- a/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommandHandler.cs +++ b/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommandHandler.cs @@ -13,7 +13,7 @@ using AipsCore.Domain.Models.Whiteboard.ValueObjects; namespace AipsCore.Application.Models.Shape.Command.CreateRectangle; -public class CreateRectangleCommandHandler : ICommandHandler +public class CreateRectangleCommandHandler : ICommandHandler { private readonly IShapeRepository _shapeRepository; private readonly IWhiteboardRepository _whiteboardRepository; @@ -28,11 +28,12 @@ public class CreateRectangleCommandHandler : ICommandHandler Handle(CreateRectangleCommand command, CancellationToken cancellationToken = default) + public async Task Handle(CreateRectangleCommand command, CancellationToken cancellationToken = default) { Validate(command); var rectangle = Rectangle.Create( + command.Id, command.WhiteboardId, command.AuthorId, command.PositionX, command.PositionY, @@ -43,8 +44,6 @@ public class CreateRectangleCommandHandler : ICommandHandler; \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/Whiteboard/Query/GetWhiteboardInfoRT/GetWhiteboardInfoRTQueryHandler.cs b/dotnet/AipsCore/Application/Models/Whiteboard/Query/GetWhiteboardInfoRT/GetWhiteboardInfoRTQueryHandler.cs new file mode 100644 index 0000000..142245a --- /dev/null +++ b/dotnet/AipsCore/Application/Models/Whiteboard/Query/GetWhiteboardInfoRT/GetWhiteboardInfoRTQueryHandler.cs @@ -0,0 +1,40 @@ +using AipsCore.Application.Abstract.Query; +using AipsCore.Domain.Common.Validation; +using AipsCore.Domain.Models.Whiteboard.Validation; +using AipsCore.Domain.Models.Whiteboard.ValueObjects; +using AipsCore.Infrastructure.Persistence.Db; +using Microsoft.EntityFrameworkCore; + +namespace AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardInfoRT; + +public class GetWhiteboardInfoRTQueryHandler + : IQueryHandler +{ + private readonly AipsDbContext _context; + + public GetWhiteboardInfoRTQueryHandler(AipsDbContext context) + { + _context = context; + } + + public async Task Handle(GetWhiteboardInfoRTQuery query, CancellationToken cancellationToken = default) + { + var whiteboard = await GetQuery(query.WhiteboardId).FirstOrDefaultAsync(cancellationToken); + + if (whiteboard is null) + { + throw new ValidationException(WhiteboardErrors.NotFound(new WhiteboardId(query.WhiteboardId.ToString()))); + } + + return whiteboard; + } + + private IQueryable GetQuery(Guid whiteboardId) + { + return _context.Whiteboards + .Where(w => w.Id == whiteboardId) + .Include(w => w.Memberships) + .Include(w => w.Owner) + .Include(w => w.Shapes); + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/MessageBroking/RabbitMQ/RabbitMqSubscriber.cs b/dotnet/AipsCore/Infrastructure/MessageBroking/RabbitMQ/RabbitMqSubscriber.cs index 6002ab9..936a0bc 100644 --- a/dotnet/AipsCore/Infrastructure/MessageBroking/RabbitMQ/RabbitMqSubscriber.cs +++ b/dotnet/AipsCore/Infrastructure/MessageBroking/RabbitMQ/RabbitMqSubscriber.cs @@ -30,6 +30,7 @@ public class RabbitMqSubscriber : IMessageSubscriber await channel.QueueDeclareAsync( queue: GetQueueName(), + autoDelete: false, durable: true); await channel.QueueBindAsync( diff --git a/dotnet/AipsRT/AipsRT.csproj b/dotnet/AipsRT/AipsRT.csproj index 3cb3a89..d4c6e59 100644 --- a/dotnet/AipsRT/AipsRT.csproj +++ b/dotnet/AipsRT/AipsRT.csproj @@ -6,4 +6,16 @@ enable + + + + + + + + + + + + diff --git a/dotnet/AipsRT/Hubs/TestHub.cs b/dotnet/AipsRT/Hubs/TestHub.cs index a04e8d7..2c263f1 100644 --- a/dotnet/AipsRT/Hubs/TestHub.cs +++ b/dotnet/AipsRT/Hubs/TestHub.cs @@ -1,9 +1,19 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; namespace AipsRT.Hubs; +[Authorize] public class TestHub : Hub { + public override async Task OnConnectedAsync() + { + Console.WriteLine($"LOOOOOOOOOG: [{Context.UserIdentifier}] User identifier connected"); + Console.WriteLine($"LOOOOOG222: [{Context.User?.Identity?.Name}] User identity name connected"); + + await base.OnConnectedAsync(); + } + public async Task SendText(string text) { await Clients.All.SendAsync("ReceiveText", text); diff --git a/dotnet/AipsRT/Hubs/WhiteboardHub.cs b/dotnet/AipsRT/Hubs/WhiteboardHub.cs new file mode 100644 index 0000000..6322be0 --- /dev/null +++ b/dotnet/AipsRT/Hubs/WhiteboardHub.cs @@ -0,0 +1,57 @@ +using AipsCore.Application.Abstract.MessageBroking; +using AipsRT.Model.Whiteboard; +using AipsRT.Model.Whiteboard.Shapes; +using AipsRT.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; + +namespace AipsRT.Hubs; + +[Authorize] +public class WhiteboardHub : Hub +{ + private readonly WhiteboardManager _whiteboardManager; + private readonly IMessagingService _messagingService; + + public WhiteboardHub(WhiteboardManager whiteboardManager, IMessagingService messagingService) + { + _whiteboardManager = whiteboardManager; + _messagingService = messagingService; + } + + public async Task JoinWhiteboard(Guid whiteboardId) + { + if (!_whiteboardManager.WhiteboardExists(whiteboardId)) + await _whiteboardManager.AddWhiteboard(whiteboardId); + + await Groups.AddToGroupAsync(Context.ConnectionId, whiteboardId.ToString()); + + var state = _whiteboardManager.GetWhiteboard(whiteboardId)!; + + _whiteboardManager.AddUserToWhiteboard(Guid.Parse(Context.UserIdentifier!), whiteboardId); + + await Clients.Caller.SendAsync("InitWhiteboard", state); + await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId) + .SendAsync("Joined", Context.UserIdentifier!); + } + + public async Task LeaveWhiteboard(Guid whiteboardId) + { + await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId) + .SendAsync("Leaved", Context.UserIdentifier!); + } + + public async Task AddRectangle(Rectangle rectangle) + { + var whiteboard = _whiteboardManager.GetWhiteboardForUser(Guid.Parse(Context.UserIdentifier!))!; + + rectangle.OwnerId = Guid.Parse(Context.UserIdentifier!); + + await _messagingService.CreatedRectangle(whiteboard.WhiteboardId, rectangle); + + whiteboard.AddRectangle(rectangle); + + await Clients.GroupExcept(whiteboard.WhiteboardId.ToString(), Context.ConnectionId) + .SendAsync("AddedRectangle", rectangle); + } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Model/Whiteboard/GetWhiteboardService.cs b/dotnet/AipsRT/Model/Whiteboard/GetWhiteboardService.cs new file mode 100644 index 0000000..1ae70f7 --- /dev/null +++ b/dotnet/AipsRT/Model/Whiteboard/GetWhiteboardService.cs @@ -0,0 +1,52 @@ +using AipsCore.Application.Abstract; +using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardInfoRT; +using AipsCore.Domain.Models.Shape.Enums; +using AipsRT.Model.Whiteboard.Shapes.Map; + +namespace AipsRT.Model.Whiteboard; + +public class GetWhiteboardService +{ + private readonly IDispatcher _dispatcher; + + public GetWhiteboardService(IDispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + public async Task GetWhiteboard(Guid whiteboardId) + { + var query = new GetWhiteboardInfoRTQuery(whiteboardId); + return Map(await _dispatcher.Execute(query)); + } + + private static Whiteboard Map(AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard entity) + { + var whiteboard = new Whiteboard() + { + WhiteboardId = entity.Id, + OwnerId = entity.OwnerId, + }; + + foreach (var shape in entity.Shapes) + { + switch (shape.Type) + { + case ShapeType.Rectangle: + whiteboard.AddRectangle(shape.ToRectangle()); + break; + case ShapeType.Arrow: + whiteboard.AddArrow(shape.ToArrow()); + break; + case ShapeType.Line: + whiteboard.AddLine(shape.ToLine()); + break; + case ShapeType.Text: + whiteboard.AddTextShape(shape.ToTextShape()); + break; + } + } + + return whiteboard; + } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Model/Whiteboard/Shapes/Arrow.cs b/dotnet/AipsRT/Model/Whiteboard/Shapes/Arrow.cs new file mode 100644 index 0000000..8019640 --- /dev/null +++ b/dotnet/AipsRT/Model/Whiteboard/Shapes/Arrow.cs @@ -0,0 +1,10 @@ +using AipsRT.Model.Whiteboard.Structs; + +namespace AipsRT.Model.Whiteboard.Shapes; + +public class Arrow : Shape +{ + public Position EndPosition { get; set; } + + public int Thickness { get; set; } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Model/Whiteboard/Shapes/Line.cs b/dotnet/AipsRT/Model/Whiteboard/Shapes/Line.cs new file mode 100644 index 0000000..c80c464 --- /dev/null +++ b/dotnet/AipsRT/Model/Whiteboard/Shapes/Line.cs @@ -0,0 +1,10 @@ +using AipsRT.Model.Whiteboard.Structs; + +namespace AipsRT.Model.Whiteboard.Shapes; + +public class Line : Shape +{ + public Position EndPosition { get; set; } + + public int Thickness { get; set; } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Model/Whiteboard/Shapes/Map/ShapeMappingExtensions.cs b/dotnet/AipsRT/Model/Whiteboard/Shapes/Map/ShapeMappingExtensions.cs new file mode 100644 index 0000000..d463a80 --- /dev/null +++ b/dotnet/AipsRT/Model/Whiteboard/Shapes/Map/ShapeMappingExtensions.cs @@ -0,0 +1,57 @@ +using AipsRT.Model.Whiteboard.Structs; + +namespace AipsRT.Model.Whiteboard.Shapes.Map; + +public static class ShapeMappingExtensions +{ + extension(AipsCore.Infrastructure.Persistence.Shape.Shape shape) + { + public Rectangle ToRectangle() + { + return new Rectangle() + { + Id = shape.Id, + Position = new Position(shape.PositionX, shape.PositionY), + Color = shape.Color, + EndPosition = new Position(shape.EndPositionX!.Value, shape.EndPositionY!.Value), + BorderThickness = shape.Thickness!.Value, + }; + } + + public Arrow ToArrow() + { + return new Arrow() + { + Id = shape.Id, + Position = new Position(shape.PositionX, shape.PositionY), + Color = shape.Color, + EndPosition = new Position(shape.EndPositionX!.Value, shape.EndPositionY!.Value), + Thickness = shape.Thickness!.Value, + }; + } + + public Line ToLine() + { + return new Line() + { + Id = shape.Id, + Position = new Position(shape.PositionX, shape.PositionY), + Color = shape.Color, + EndPosition = new Position(shape.EndPositionX!.Value, shape.EndPositionY!.Value), + Thickness = shape.Thickness!.Value, + }; + } + + public TextShape ToTextShape() + { + return new TextShape() + { + Id = shape.Id, + Position = new Position(shape.PositionX, shape.PositionY), + Color = shape.Color, + TextValue = shape.TextValue!, + TextSize = shape.TextSize!.Value + }; + } + } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Model/Whiteboard/Shapes/Rectangle.cs b/dotnet/AipsRT/Model/Whiteboard/Shapes/Rectangle.cs new file mode 100644 index 0000000..bbd1fcd --- /dev/null +++ b/dotnet/AipsRT/Model/Whiteboard/Shapes/Rectangle.cs @@ -0,0 +1,10 @@ +using AipsRT.Model.Whiteboard.Structs; + +namespace AipsRT.Model.Whiteboard.Shapes; + +public class Rectangle : Shape +{ + public Position EndPosition { get; set; } + + public int BorderThickness { get; set; } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Model/Whiteboard/Shapes/Shape.cs b/dotnet/AipsRT/Model/Whiteboard/Shapes/Shape.cs new file mode 100644 index 0000000..cf41042 --- /dev/null +++ b/dotnet/AipsRT/Model/Whiteboard/Shapes/Shape.cs @@ -0,0 +1,14 @@ +using AipsRT.Model.Whiteboard.Structs; + +namespace AipsRT.Model.Whiteboard.Shapes; + +public abstract class Shape +{ + public Guid Id { get; set; } + + public Guid OwnerId { get; set; } + + public Position Position { get; set; } + + public string Color { get; set; } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Model/Whiteboard/Shapes/TextShape.cs b/dotnet/AipsRT/Model/Whiteboard/Shapes/TextShape.cs new file mode 100644 index 0000000..dde1a79 --- /dev/null +++ b/dotnet/AipsRT/Model/Whiteboard/Shapes/TextShape.cs @@ -0,0 +1,10 @@ +using AipsRT.Model.Whiteboard.Shapes; + +namespace AipsRT.Model.Whiteboard.Shapes; + +public class TextShape : Shape +{ + public string TextValue { get; set; } + + public int TextSize { get; set; } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Model/Whiteboard/Structs/Position.cs b/dotnet/AipsRT/Model/Whiteboard/Structs/Position.cs new file mode 100644 index 0000000..2fc4ab6 --- /dev/null +++ b/dotnet/AipsRT/Model/Whiteboard/Structs/Position.cs @@ -0,0 +1,13 @@ +namespace AipsRT.Model.Whiteboard.Structs; + +public struct Position +{ + public int X { get; set; } + public int Y { get; set; } + + public Position(int x, int y) + { + X = x; + Y = y; + } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Model/Whiteboard/Whiteboard.cs b/dotnet/AipsRT/Model/Whiteboard/Whiteboard.cs new file mode 100644 index 0000000..074a955 --- /dev/null +++ b/dotnet/AipsRT/Model/Whiteboard/Whiteboard.cs @@ -0,0 +1,41 @@ +using AipsRT.Model.Whiteboard.Shapes; + +namespace AipsRT.Model.Whiteboard; + +public class Whiteboard +{ + public Guid WhiteboardId { get; set; } + + public Guid OwnerId { get; set; } + + public List Shapes { get; } = []; + + public List Rectangles { get; } = []; + public List Arrows { get; } = []; + public List Lines { get; } = []; + public List TextShapes { get; } = []; + + public void AddRectangle(Rectangle shape) + { + Shapes.Add(shape); + Rectangles.Add(shape); + } + + public void AddArrow(Arrow shape) + { + Shapes.Add(shape); + Arrows.Add(shape); + } + + public void AddLine(Line shape) + { + Shapes.Add(shape); + Lines.Add(shape); + } + + public void AddTextShape(TextShape shape) + { + Shapes.Add(shape); + TextShapes.Add(shape); + } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Model/Whiteboard/WhiteboardManager.cs b/dotnet/AipsRT/Model/Whiteboard/WhiteboardManager.cs new file mode 100644 index 0000000..10c19dc --- /dev/null +++ b/dotnet/AipsRT/Model/Whiteboard/WhiteboardManager.cs @@ -0,0 +1,59 @@ +using System.Collections.Concurrent; +using AipsCore.Application.Abstract; + +namespace AipsRT.Model.Whiteboard; + +public class WhiteboardManager +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ConcurrentDictionary _whiteboards = new(); + private readonly ConcurrentDictionary _userInWhiteboards = new(); + + public WhiteboardManager(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + } + + public async Task AddWhiteboard(Guid whiteboardId) + { + var getWhiteboardService = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + var whiteboard = await getWhiteboardService.GetWhiteboard(whiteboardId); + + _whiteboards[whiteboardId] = whiteboard; + } + + public bool WhiteboardExists(Guid whiteboardId) + { + return _whiteboards.ContainsKey(whiteboardId); + } + + public void RemoveWhiteboard(Guid whiteboardId) + { + _whiteboards.TryRemove(whiteboardId, out _); + } + + public Whiteboard? GetWhiteboard(Guid whiteboardId) + { + return _whiteboards.GetValueOrDefault(whiteboardId); + } + + public void AddUserToWhiteboard(Guid userId, Guid whiteboardId) + { + _userInWhiteboards[userId] = whiteboardId; + } + + public Guid GetUserWhiteboard(Guid userId) + { + return _userInWhiteboards[userId]; + } + + public void RemoveUserFromWhiteboard(Guid userId, Guid whiteboardId) + { + _userInWhiteboards.TryRemove(whiteboardId, out _); + } + + public Whiteboard? GetWhiteboardForUser(Guid userId) + { + return GetWhiteboard(GetUserWhiteboard(userId)); + } +} \ No newline at end of file diff --git a/dotnet/AipsRT/Program.cs b/dotnet/AipsRT/Program.cs index ab84619..0995a0a 100644 --- a/dotnet/AipsRT/Program.cs +++ b/dotnet/AipsRT/Program.cs @@ -1,10 +1,25 @@ +using AipsCore.Infrastructure.DI; using AipsRT.Hubs; +using AipsRT.Model.Whiteboard; +using AipsRT.Services; +using AipsRT.Services.Interfaces; +using DotNetEnv; using Microsoft.AspNetCore.SignalR; +Env.Load("../../.env"); + var builder = WebApplication.CreateBuilder(args); +builder.Configuration.AddEnvironmentVariables(); + builder.Services.AddSignalR(); +builder.Services.AddAips(builder.Configuration); + +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + builder.Services.AddCors(options => { options.AddPolicy("frontend", @@ -26,6 +41,11 @@ app.MapGet("/test", (IHubContext hubContext) => }); app.UseCors("frontend"); -app.MapHub("/testhub"); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapHub("/hubs/test"); +app.MapHub("/hubs/whiteboard"); app.Run(); \ No newline at end of file diff --git a/dotnet/AipsRT/Services/Interfaces/IMessagingService.cs b/dotnet/AipsRT/Services/Interfaces/IMessagingService.cs new file mode 100644 index 0000000..de1f413 --- /dev/null +++ b/dotnet/AipsRT/Services/Interfaces/IMessagingService.cs @@ -0,0 +1,8 @@ +using AipsRT.Model.Whiteboard.Shapes; + +namespace AipsRT.Services.Interfaces; + +public interface IMessagingService +{ + Task CreatedRectangle(Guid whiteboardId, Rectangle rectangle); +} \ No newline at end of file diff --git a/dotnet/AipsRT/Services/MessagingService.cs b/dotnet/AipsRT/Services/MessagingService.cs new file mode 100644 index 0000000..980fae3 --- /dev/null +++ b/dotnet/AipsRT/Services/MessagingService.cs @@ -0,0 +1,36 @@ +using AipsCore.Application.Abstract.MessageBroking; +using AipsCore.Application.Common.Message.AddRectangle; +using AipsCore.Application.Models.Shape.Command.CreateRectangle; +using AipsRT.Model.Whiteboard.Shapes; +using AipsRT.Services.Interfaces; + +namespace AipsRT.Services; + +public class MessagingService : IMessagingService +{ + private readonly IMessagePublisher _messagePublisher; + + public MessagingService(IMessagePublisher messagePublisher) + { + _messagePublisher = messagePublisher; + } + + public async Task CreatedRectangle(Guid whiteboardId, Rectangle rectangle) + { + var command = new CreateRectangleCommand( + rectangle.Id.ToString(), + whiteboardId.ToString(), + rectangle.OwnerId.ToString(), + rectangle.Position.X, + rectangle.Position.Y, + rectangle.Color, + rectangle.EndPosition.X, + rectangle.EndPosition.Y, + rectangle.BorderThickness + ); + + var message = new AddRectangleMessage(command); + + await _messagePublisher.PublishAsync(message); + } +} \ No newline at end of file diff --git a/dotnet/AipsWorker/WorkerService.cs b/dotnet/AipsWorker/WorkerService.cs index dea0790..48060c5 100644 --- a/dotnet/AipsWorker/WorkerService.cs +++ b/dotnet/AipsWorker/WorkerService.cs @@ -2,6 +2,7 @@ using System.Reflection; using AipsCore.Application.Abstract; using AipsCore.Application.Abstract.MessageBroking; using AipsCore.Application.Common.Message.TestMessage; +using AipsCore.Domain.Common.Validation; using AipsWorker.Utilities; using Microsoft.Extensions.Hosting; @@ -48,7 +49,26 @@ public class WorkerService : BackgroundService private async Task HandleMessage(T message, CancellationToken ct) where T : IMessage { - await _dispatcher.Execute(message, ct); + try + { + await _dispatcher.Execute(message, ct); + + Console.WriteLine($"OK: {message.GetType().Name}"); + } + catch (ValidationException validationException) + { + Console.WriteLine("===Validation Exception: "); + foreach (var error in validationException.ValidationErrors) + { + Console.WriteLine(" * Code: " + error.Code); + Console.WriteLine(" * Message: " + error.Message); + Console.WriteLine("==================="); + } + } + catch (Exception ex) + { + Console.WriteLine("Unhandled Exception: " + ex.Message); + } } private MethodInfo GetMessageHandleMethod(Type messageType) diff --git a/front/src/App.vue b/front/src/App.vue index 9d6fbef..c6ab3c9 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -1,11 +1,20 @@ diff --git a/front/src/components/whiteboard/WhiteboardCanvas.vue b/front/src/components/whiteboard/WhiteboardCanvas.vue new file mode 100644 index 0000000..cc5aafd --- /dev/null +++ b/front/src/components/whiteboard/WhiteboardCanvas.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/front/src/components/whiteboard/WhiteboardToolbar.vue b/front/src/components/whiteboard/WhiteboardToolbar.vue new file mode 100644 index 0000000..904622c --- /dev/null +++ b/front/src/components/whiteboard/WhiteboardToolbar.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/front/src/components/whiteboard/shapes/SvgDraftRect.vue b/front/src/components/whiteboard/shapes/SvgDraftRect.vue new file mode 100644 index 0000000..17632e1 --- /dev/null +++ b/front/src/components/whiteboard/shapes/SvgDraftRect.vue @@ -0,0 +1,22 @@ + + + diff --git a/front/src/components/whiteboard/shapes/SvgRectangle.vue b/front/src/components/whiteboard/shapes/SvgRectangle.vue new file mode 100644 index 0000000..95aedb3 --- /dev/null +++ b/front/src/components/whiteboard/shapes/SvgRectangle.vue @@ -0,0 +1,17 @@ + + + diff --git a/front/src/router/index.ts b/front/src/router/index.ts index af8f62f..71145fe 100644 --- a/front/src/router/index.ts +++ b/front/src/router/index.ts @@ -35,6 +35,12 @@ const router = createRouter({ component: () => import('../views/SignupView.vue'), meta: { guestOnly: true }, }, + { + path: '/whiteboard/:id', + name: 'whiteboard', + component: () => import('../views/WhiteboardView.vue'), + meta: { requiresAuth: true, hideTopBar: true }, + }, ], }) diff --git a/front/src/services/signalr.ts b/front/src/services/signalr.ts index be82ea9..0477c6b 100644 --- a/front/src/services/signalr.ts +++ b/front/src/services/signalr.ts @@ -3,6 +3,7 @@ import { HubConnectionBuilder, HubConnectionState, } from "@microsoft/signalr"; +import {useAuthStore} from "@/stores/auth.ts"; export class SignalRService { private connection: HubConnection; @@ -10,8 +11,12 @@ export class SignalRService { constructor( hubUrl: string, ) { + const authStore = useAuthStore(); + this.connection = new HubConnectionBuilder() - .withUrl(hubUrl) + .withUrl(hubUrl, { + accessTokenFactory: () => authStore.accessToken! + }) .withAutomaticReconnect() .build(); } diff --git a/front/src/services/testHubService.ts b/front/src/services/testHubService.ts index c797791..325bc94 100644 --- a/front/src/services/testHubService.ts +++ b/front/src/services/testHubService.ts @@ -2,7 +2,7 @@ import {SignalRService} from "@/services/signalr.ts"; const client = new SignalRService( - `http://localhost:5039/testhub`, + `/hubs/test`, ); export const testHubService = { diff --git a/front/src/services/whiteboardHubService.ts b/front/src/services/whiteboardHubService.ts new file mode 100644 index 0000000..92f65c0 --- /dev/null +++ b/front/src/services/whiteboardHubService.ts @@ -0,0 +1,49 @@ +import { SignalRService } from '@/services/signalr.ts' +import type { Rectangle, Whiteboard } from '@/types/whiteboard.ts' + +const client = new SignalRService(`/hubs/whiteboard`) + +export const whiteboardHubService = { + async connect() { + await client.start() + }, + + async disconnect() { + await client.stop() + }, + + async joinWhiteboard(id: string) { + await client.invoke('JoinWhiteboard', id) + }, + + async leaveWhiteboard(id: string) { + await client.invoke('LeaveWhiteboard', id) + }, + + async addRectangle(rectangle: Rectangle) { + await client.invoke('AddRectangle', rectangle) + }, + + onInitWhiteboard(callback: (whiteboard: Whiteboard) => void) { + client.on('InitWhiteboard', callback) + }, + + onAddedRectangle(callback: (rectangle: Rectangle) => void) { + client.on('AddedRectangle', callback) + }, + + onJoined(callback: (userId: string) => void) { + client.on('Joined', callback) + }, + + onLeaved(callback: (userId: string) => void) { + client.on('Leaved', callback) + }, + + offAll() { + client.off('InitWhiteboard') + client.off('AddedRectangle') + client.off('Joined') + client.off('Leaved') + }, +} diff --git a/front/src/stores/whiteboard.ts b/front/src/stores/whiteboard.ts new file mode 100644 index 0000000..1a3881a --- /dev/null +++ b/front/src/stores/whiteboard.ts @@ -0,0 +1,88 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' +import type { Rectangle, ShapeTool, Whiteboard } from '@/types/whiteboard.ts' +import { whiteboardHubService } from '@/services/whiteboardHubService.ts' + +export const useWhiteboardStore = defineStore('whiteboard', () => { + const whiteboard = ref(null) + const selectedTool = ref('rectangle') + const isConnected = ref(false) + const isLoading = ref(false) + const error = ref(null) + + async function joinWhiteboard(id: string) { + isLoading.value = true + error.value = null + + try { + await whiteboardHubService.connect() + isConnected.value = true + + whiteboardHubService.onInitWhiteboard((wb) => { + whiteboard.value = wb + isLoading.value = false + }) + + whiteboardHubService.onAddedRectangle((rectangle) => { + whiteboard.value?.rectangles.push(rectangle) + }) + + whiteboardHubService.onJoined((userId) => { + console.log('User joined:', userId) + }) + + whiteboardHubService.onLeaved((userId) => { + console.log('User left:', userId) + }) + + await whiteboardHubService.joinWhiteboard(id) + } catch (e: any) { + error.value = e?.message ?? 'Failed to join whiteboard' + isLoading.value = false + } + } + + async function leaveWhiteboard() { + if (!whiteboard.value) return + + try { + await whiteboardHubService.leaveWhiteboard(whiteboard.value.whiteboardId) + } catch (e) { + console.warn('Leave request failed', e) + } + + whiteboardHubService.offAll() + await whiteboardHubService.disconnect() + + whiteboard.value = null + isConnected.value = false + selectedTool.value = 'rectangle' + error.value = null + } + + async function addRectangle(rectangle: Rectangle) { + whiteboard.value?.rectangles.push(rectangle) + + try { + await whiteboardHubService.addRectangle(rectangle) + } catch (e: any) { + console.error('Failed to send rectangle', e) + } + } + + function selectTool(tool: ShapeTool) { + selectedTool.value = tool + } + + return { + whiteboard, + selectedTool, + isConnected, + isLoading, + error, + joinWhiteboard, + leaveWhiteboard, + addRectangle, + selectTool, + } +}) diff --git a/front/src/types/whiteboard.ts b/front/src/types/whiteboard.ts new file mode 100644 index 0000000..85818ab --- /dev/null +++ b/front/src/types/whiteboard.ts @@ -0,0 +1,42 @@ +export interface Position { + x: number + y: number +} + +export interface Shape { + id: string + ownerId: string + position: Position + color: string +} + +export interface Rectangle extends Shape { + endPosition: Position + borderThickness: number +} + +export interface Arrow extends Shape { + endPosition: Position + thickness: number +} + +export interface Line extends Shape { + endPosition: Position + thickness: number +} + +export interface TextShape extends Shape { + textValue: string + textSize: number +} + +export interface Whiteboard { + whiteboardId: string + ownerId: string + rectangles: Rectangle[] + arrows: Arrow[] + lines: Line[] + textShapes: TextShape[] +} + +export type ShapeTool = 'rectangle' | 'arrow' | 'line' | 'text' diff --git a/front/src/views/TestView.vue b/front/src/views/TestView.vue index d838e12..e75b89b 100644 --- a/front/src/views/TestView.vue +++ b/front/src/views/TestView.vue @@ -1,13 +1,22 @@ + + diff --git a/front/vite.config.ts b/front/vite.config.ts index 1a42daa..14b3282 100644 --- a/front/vite.config.ts +++ b/front/vite.config.ts @@ -21,6 +21,11 @@ export default defineConfig({ '/api': { target: 'http://localhost:5266', changeOrigin: true + }, + '/hubs/': { + target: 'http://localhost:5039', + changeOrigin: true, + ws: true } } },