From 0f0418dee31d9be940587141c30884ccb2bf4fd5 Mon Sep 17 00:00:00 2001 From: Andrija Stevanovic Date: Mon, 16 Feb 2026 16:13:43 +0100 Subject: [PATCH 1/5] frontend whiteboard canvas with rectangle support --- .../whiteboard/WhiteboardCanvas.vue | 100 ++++++++++++++++++ .../whiteboard/WhiteboardToolbar.vue | 54 ++++++++++ .../whiteboard/shapes/SvgDraftRect.vue | 22 ++++ .../whiteboard/shapes/SvgRectangle.vue | 17 +++ front/src/services/whiteboardHubService.ts | 49 +++++++++ front/src/stores/whiteboard.ts | 88 +++++++++++++++ front/src/types/whiteboard.ts | 42 ++++++++ front/src/views/WhiteboardView.vue | 45 ++++++++ 8 files changed, 417 insertions(+) create mode 100644 front/src/components/whiteboard/WhiteboardCanvas.vue create mode 100644 front/src/components/whiteboard/WhiteboardToolbar.vue create mode 100644 front/src/components/whiteboard/shapes/SvgDraftRect.vue create mode 100644 front/src/components/whiteboard/shapes/SvgRectangle.vue create mode 100644 front/src/services/whiteboardHubService.ts create mode 100644 front/src/stores/whiteboard.ts create mode 100644 front/src/types/whiteboard.ts create mode 100644 front/src/views/WhiteboardView.vue 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/services/whiteboardHubService.ts b/front/src/services/whiteboardHubService.ts new file mode 100644 index 0000000..9945d81 --- /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(`http://localhost:5039/whiteboardhub`) + +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/WhiteboardView.vue b/front/src/views/WhiteboardView.vue new file mode 100644 index 0000000..d9ff4f0 --- /dev/null +++ b/front/src/views/WhiteboardView.vue @@ -0,0 +1,45 @@ + + + From d9caeb2209d9adcd9bea67bde377b6305d75ecc6 Mon Sep 17 00:00:00 2001 From: Andrija Stevanovic Date: Mon, 16 Feb 2026 16:14:25 +0100 Subject: [PATCH 2/5] RT state managment and rectangle support --- .../GetWhiteboardInfoRTQuery.cs | 5 ++ .../GetWhiteboardInfoRTQueryHandler.cs | 40 +++++++++++++ dotnet/AipsRT/AipsRT.csproj | 12 ++++ dotnet/AipsRT/Hubs/Dtos/AddRectangleDto.cs | 1 + dotnet/AipsRT/Hubs/TestHub.cs | 10 ++++ dotnet/AipsRT/Hubs/WhiteboardHub.cs | 49 +++++++++++++++ .../Model/Whiteboard/GetWhiteboardService.cs | 52 ++++++++++++++++ .../AipsRT/Model/Whiteboard/Shapes/Arrow.cs | 10 ++++ dotnet/AipsRT/Model/Whiteboard/Shapes/Line.cs | 10 ++++ .../Shapes/Map/ShapeMappingExtensions.cs | 57 ++++++++++++++++++ .../Model/Whiteboard/Shapes/Rectangle.cs | 10 ++++ .../AipsRT/Model/Whiteboard/Shapes/Shape.cs | 14 +++++ .../Model/Whiteboard/Shapes/TextShape.cs | 10 ++++ .../Model/Whiteboard/Structs/Position.cs | 13 ++++ dotnet/AipsRT/Model/Whiteboard/Whiteboard.cs | 41 +++++++++++++ .../Model/Whiteboard/WhiteboardManager.cs | 59 +++++++++++++++++++ dotnet/AipsRT/Program.cs | 17 ++++++ front/src/App.vue | 17 ++++-- front/src/router/index.ts | 6 ++ front/src/services/signalr.ts | 7 ++- front/src/views/TestView.vue | 24 ++++++++ 21 files changed, 459 insertions(+), 5 deletions(-) create mode 100644 dotnet/AipsCore/Application/Models/Whiteboard/Query/GetWhiteboardInfoRT/GetWhiteboardInfoRTQuery.cs create mode 100644 dotnet/AipsCore/Application/Models/Whiteboard/Query/GetWhiteboardInfoRT/GetWhiteboardInfoRTQueryHandler.cs create mode 100644 dotnet/AipsRT/Hubs/Dtos/AddRectangleDto.cs create mode 100644 dotnet/AipsRT/Hubs/WhiteboardHub.cs create mode 100644 dotnet/AipsRT/Model/Whiteboard/GetWhiteboardService.cs create mode 100644 dotnet/AipsRT/Model/Whiteboard/Shapes/Arrow.cs create mode 100644 dotnet/AipsRT/Model/Whiteboard/Shapes/Line.cs create mode 100644 dotnet/AipsRT/Model/Whiteboard/Shapes/Map/ShapeMappingExtensions.cs create mode 100644 dotnet/AipsRT/Model/Whiteboard/Shapes/Rectangle.cs create mode 100644 dotnet/AipsRT/Model/Whiteboard/Shapes/Shape.cs create mode 100644 dotnet/AipsRT/Model/Whiteboard/Shapes/TextShape.cs create mode 100644 dotnet/AipsRT/Model/Whiteboard/Structs/Position.cs create mode 100644 dotnet/AipsRT/Model/Whiteboard/Whiteboard.cs create mode 100644 dotnet/AipsRT/Model/Whiteboard/WhiteboardManager.cs diff --git a/dotnet/AipsCore/Application/Models/Whiteboard/Query/GetWhiteboardInfoRT/GetWhiteboardInfoRTQuery.cs b/dotnet/AipsCore/Application/Models/Whiteboard/Query/GetWhiteboardInfoRT/GetWhiteboardInfoRTQuery.cs new file mode 100644 index 0000000..868d8b9 --- /dev/null +++ b/dotnet/AipsCore/Application/Models/Whiteboard/Query/GetWhiteboardInfoRT/GetWhiteboardInfoRTQuery.cs @@ -0,0 +1,5 @@ +using AipsCore.Application.Abstract.Query; + +namespace AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardInfoRT; + +public record GetWhiteboardInfoRTQuery(Guid WhiteboardId) : IQuery; \ 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/AipsRT/AipsRT.csproj b/dotnet/AipsRT/AipsRT.csproj index 3cb3a89..f311397 100644 --- a/dotnet/AipsRT/AipsRT.csproj +++ b/dotnet/AipsRT/AipsRT.csproj @@ -6,4 +6,16 @@ enable + + + + + + + + + + + + diff --git a/dotnet/AipsRT/Hubs/Dtos/AddRectangleDto.cs b/dotnet/AipsRT/Hubs/Dtos/AddRectangleDto.cs new file mode 100644 index 0000000..3ba488c --- /dev/null +++ b/dotnet/AipsRT/Hubs/Dtos/AddRectangleDto.cs @@ -0,0 +1 @@ +namespace AipsRT.Hubs.Dtos; \ No newline at end of file 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..f70af4f --- /dev/null +++ b/dotnet/AipsRT/Hubs/WhiteboardHub.cs @@ -0,0 +1,49 @@ +using AipsRT.Model.Whiteboard; +using AipsRT.Model.Whiteboard.Shapes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; + +namespace AipsRT.Hubs; + +[Authorize] +public class WhiteboardHub : Hub +{ + private readonly WhiteboardManager _whiteboardManager; + + public WhiteboardHub(WhiteboardManager whiteboardManager) + { + _whiteboardManager = whiteboardManager; + } + + 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!))!; + + 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..c980533 100644 --- a/dotnet/AipsRT/Program.cs +++ b/dotnet/AipsRT/Program.cs @@ -1,10 +1,22 @@ +using AipsCore.Infrastructure.DI; using AipsRT.Hubs; +using AipsRT.Model.Whiteboard; +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.AddCors(options => { options.AddPolicy("frontend", @@ -26,6 +38,11 @@ app.MapGet("/test", (IHubContext hubContext) => }); app.UseCors("frontend"); + +app.UseAuthentication(); +app.UseAuthorization(); + app.MapHub("/testhub"); +app.MapHub("/whiteboardhub"); app.Run(); \ No newline at end of file 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/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/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 @@